并发编程-线程的启动、死锁、线程安全、ThreadLocal

971 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情


1 线程的启动方式

线程的启动方式只有两种。

方式1:继承Thread,然后调用start()启动。

private static class PrimeThread extends Thread {
    @Override
    public void run() {
        System.out.println("thread extend Thread---name:" + Thread.currentThread().getName());
    }
}

PrimeThread thread = new PrimeThread();
thread.start();

方式2:实现Runnable,然后交给Thread去启动。

private static class PrimeRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("thread implements Runnable---name:" + Thread.currentThread().getName());
    }
}

PrimeRunnable runnable = new PrimeRunnable();
new Thread(runnable).start();

其他的比如线程池、FutureTask等都属于这两种的包装或封装。

并且Thread源码的注释中也清楚的写了有两种方式创建线程:

* <p>
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
    * <code>Thread</code>. An instance of the subclass can then be
    * allocated and started. For example, a thread that computes primes
    * larger than a stated value could be written as follows:
* <hr><blockquote><pre>
*     class PrimeThread extends Thread {
*         long minPrime;
*         PrimeThread(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
*     PrimeThread p = new PrimeThread(143);
*     p.start();
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
    * implements the <code>Runnable</code> interface. That class then
        * implements the <code>run</code> method. An instance of the class can
            * then be allocated, passed as an argument when creating
            * <code>Thread</code>, and started. The same example in this other
            * style looks like the following:
* <hr><blockquote><pre>
*     class PrimeRun implements Runnable {
*         long minPrime;
*         PrimeRun(long minPrime) {
*             this.minPrime = minPrime;
*         }
*
*         public void run() {
*             // compute primes larger than minPrime
*             &nbsp;.&nbsp;.&nbsp;.
*         }
*     }
* </pre></blockquote><hr>
 * <p>

2 线程的状态

Java中线程的状态分为6种:

1、初始(NEW):新创建了一个线程,但是还没有调用start()方法。

2、运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种装填笼统的称为“运行”。

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,获取CPU的使用权,此时处于就绪状态(READY),就绪状态的线程在获得CPU时间片后变为运行中状态(RUNNING)。

3、阻塞(BLOCKED):表示线程阻塞于锁。

4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5、超时等待(TIMED_WAITING):该状态不同于WATING,它可以在指定的时间后自行返回。

6、终止(TERMINATED):表示该线程已经执行完毕。

线程生命周期如下:

3 死锁

3.1 概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。

死锁还有几个要求:

  1. 争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁。
  2. 争夺者拿到资源不放手。

3.1.1 学术定义

死锁的发生必须具备以下四个必要条件。

  1. 互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  2. 请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  3. 不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  4. 环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。

  • 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
  • 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
  • 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
  • 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

避免死锁常见的算法有有序资源分配法、银行家算法。

示例代码:

/**
 * @Description: 死锁的产生
 * @CreateDate: 2022/3/15 2:31 下午
 */
public class NormalDeadLock {

    /**
     * 第1个锁
     */
    private static final Object LOCK_1 = new Object();
    /**
     * 第2个锁
     */
    private static final Object LOCK_2 = new Object();

    /**
     * 第1个拿锁的方法 先去拿锁1,再去拿锁2
     *
     * @throws InterruptedException 中断异常
     */
    private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
    }

    /**
     * 第2个拿锁的方法 先去拿锁2,再去拿锁1,这就导致方法1和方法2各拿一个锁,然后互不相让,都不释放自己的锁,造成了互斥,就产生了死锁
     *
     * @throws InterruptedException 中断异常
     */
    private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_2) {
            System.out.println(threadName + " get LOCK_2");
            Thread.sleep(100);
            synchronized (LOCK_1) {
                System.out.println(threadName + " get LOCK_1");
            }
        }
    }

    /**
     * 子线程PrimeThread1
     */
    private static class PrimeThread1 extends Thread {
        private final String name;

        public PrimeThread1(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 子线程PrimeThread2
     */
    private static class PrimeThread2 extends Thread {
        private final String name;

        public PrimeThread2(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
        PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

        thread1.start();
        thread2.start();
    }

}

执行后,可以看到控制台没有结束运行,看不到Process finished with exit code 0,但是又一直处于静止状态。

PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2

3.2 危害

  1. 线程不工作了,但是整个程序还是活着的。
  2. 没有任何的异常信息可以供我们检查。
  3. 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。

3.3 解决方案

关键是保证拿锁的顺序一致。

两种解决方式:

1、内部通过顺序比较,确定拿锁的顺序。

比如上述示例代码中,可以让方法1和方法2同时都先拿锁1,然后再去拿锁2,就能解决死锁问题。

private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
}

private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (LOCK_1) {
            System.out.println(threadName + " get LOCK_1");
            Thread.sleep(100);
            synchronized (LOCK_2) {
                System.out.println(threadName + " get LOCK_2");
            }
        }
}

修改后后,可以看到程序能正常执行。

PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
PrimeThread2 get LOCK_1
PrimeThread2 get LOCK_2

Process finished with exit code 0

2、采用尝试拿锁的机制。

示例代码:

/**
 * @Description: 尝试拿锁,解决死锁问题
 * @CreateDate: 2022/3/15 2:57 下午
 */
public class TryGetLock {
    /**
     * 第1个锁
     */
    private static final Lock LOCK_1 = new ReentrantLock();
    /**
     * 第2个锁
     */
    private static final Lock LOCK_2 = new ReentrantLock();

    /**
     * 方法1 先尝试拿锁1,再尝试拿锁2,拿不到锁2的话连同锁1一起释放
     *
     * @throws InterruptedException 中断异常
     */
    private static void method1() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (LOCK_1.tryLock()) {
                System.out.println(threadName + " get LOCK_1");
                try {
                    if (LOCK_2.tryLock()) {
                        try {
                            System.out.println(threadName + " get LOCK_2");
                            System.out.println("method1 do working...");
                            break;
                        } finally {
                            LOCK_2.unlock();
                        }
                    }
                } finally {
                    LOCK_1.unlock();
                }
            }
            //注意:这里需要给个很短的间隔时间去让其他线程拿锁,不然可能会造成活锁
            Thread.sleep(r.nextInt(3));
        }
    }

    /**
     * 方法2 先尝试拿锁2,再尝试拿锁1,拿不到锁1的话连同锁2一起释放
     *
     * @throws InterruptedException 中断异常
     */
    private static void method2() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        Random r = new Random();
        while (true) {
            if (LOCK_2.tryLock()) {
                System.out.println(threadName + " get LOCK_2");
                try {
                    if (LOCK_1.tryLock()) {
                        try {
                            System.out.println(threadName + " get LOCK_1");
                            System.out.println("method2 do working...");
                            break;
                        } finally {
                            LOCK_1.unlock();
                        }
                    }
                } finally {
                    LOCK_2.unlock();
                }
            }
            Thread.sleep(r.nextInt(3));
        }
    }

    private static class PrimeThread1 extends Thread {
        private final String name;

        public PrimeThread1(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method1();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class PrimeThread2 extends Thread {
        private final String name;

        public PrimeThread2(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
        PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

        thread1.start();
        thread2.start();
    }
}

执行结果:

PrimeThread2 get LOCK_2
PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2
PrimeThread2 get LOCK_1
method2 do working...
PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
method1 do working...

Process finished with exit code 0

4 其他线程安全问题

4.1 活锁

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

如上边的尝试拿锁示例代码中,如果不加随机sleep,就会造成活锁。

4.2 线程饥饿

低优先级的线程,总是拿不到执行时间。

5 ThreadLocal

5.1 与Synchonized的比较

ThreadLocalsynchonized都用于解决多线程并发訪问。但是ThreadLocalsynchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

5.2 ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法:

  • protected T initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  • public void set(T value)设置当前线程的线程局部变量。
  • public T get()返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
public final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();

THREAD_LOCAL代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。

示例代码:

/**
 * @Description: 使用ThreadLocal
 * @CreateDate: 2022/3/15 3:37 下午
 */
public class UseThreadLocal {

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private void startThreadArray() {
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new PrimeRunnable(i));
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }

    private static class PrimeRunnable implements Runnable {
        private final int id;

        public PrimeRunnable(int id) {
            this.id = id;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            THREAD_LOCAL.set("线程" + id);
            System.out.println(threadName + ":" + THREAD_LOCAL.get());
        }
    }

    public static void main(String[] args) {
        UseThreadLocal useThreadLocal = new UseThreadLocal();
        useThreadLocal.startThreadArray();
    }
}

5.3 ThreadLocal的内部实现

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
ThreadLocal.ThreadLocalMap threadLocals = null;

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMapThreadLocalMapThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。

看下ThreadLocal的内部类ThreadLocalMap源码:

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            
            //类似于map的key、value结构,key就是ThreadLocal,value就是要隔离访问的变量
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * 用数组保存了Entry,因为可能有多个变量需要线程隔离访问
         */
        private Entry[] table;

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // Android-changed: Use refersTo()
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

回顾get方法,其实就是拿到每个线程独有的ThreadLocalMap,然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行Map的创建,初始化等工作。

关注木水小站 (zhangmushui.cn)和微信公众号【木水Code】,及时获取更多最新技术干货。