Java 编程之美:并发编程

借用 Java 并发编程实践中的话,编写正确的程序并不容易,而编写正常的并发程序就更难了!

相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的。

并发编程相比 Java 中其他知识点学习起来门槛相对较高,学习起来比较费劲,从而导致很多人望而却步;

而无论是职场面试和高并发高流量的系统的实现却都还离不开并发编程,从而导致能够真正掌握并发编程的人才成为市场比较迫切需求的。

通过通俗易懂的方式来和大家聊聊多线程并发编程中涉及到的高级基础知识

  • 什么是多线程并发和并行。

  • 什么是线程安全问题。

  • 什么是共享变量的内存可见性问题。

  • 什么是 Java 中原子性操作。

  • 什么是 Java 中的 CAS 操作,AtomicLong 实现原理

  • 什么是 Java 指令重排序。

  • Java 中 Synchronized 关键字的内存语义是什么。

  • Java 中 Volatile 关键字的内存语义是什么。

  • 什么是伪共享,为何会出现,以及如何避免。

  • 什么是可重入锁、乐观锁、悲观锁、公平锁、非公平锁、独占锁、共享锁。

多线程并发与并行

首先要澄清并发和并行的概念,并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束;而并行是说在单位时间内多个任务同时在执行;

并发任务强调在一个时间段内同时执行,而一个时间段有多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行。

在单个 CPU 的时代多个任务同时运行都是并发,这是因为 CPU 同时只能执行一个任务,单个 CPU 时代多任务是共享一个 CPU 的,当一个任务占用 CPU 运行时候,其它任务就会被挂起,当占用 CPU 的任务时间片用完后,会把 CPU 让给其它任务来使用。

所以在单 CPU 时代多线程编程的意义不大,并且线程间频繁的上下文切换还会带来开销。

如下图单个 CPU 上运行两个线程,可知线程 A 和 B 是轮流使用 CPU 进行任务处理的,也就是同时 CPU 只在执行一个线程上面的任务,当前线程 A 的时间片用完后会进行线程上下文切换,也就是保存当前线程的执行线程,然后切换线程 B 占用 CPU 运行任务。

Java 编程之美:并发编程_第1张图片


如下图双 CPU 时候,线程 A 和线程 B 在自己的 CPU 上执行任务,实现了真正的并行运行。

Java 编程之美:并发编程_第2张图片

而在多线程编程实践中线程的个数往往多于 CPU 的个数,所以平时都是称多线程并发编程而不是多线程并行编程。

线程安全问题

谈到线程安全问题不得不先说说什么是共享资源,所谓共享资源是说多个线程都可以去访问的资源。

线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施的时候,导致脏数据或者其它不可预见的结果的问题。

Java 编程之美:并发编程_第3张图片

如上图,线程 A 和线程 B 可以同时去操作主内存中的共享变量,是不是说多个线程共享了资源,都会产生线程安全问题呢?

答案是否定的,如果多个线程都是只读取共享资源,而不去修改,那么就不会存在线程安全问题。

只有当至少一个线程修改共享资源时候才会存在线程安全问题。最典型的就是计数器类的实现,计数 count 本身是一个共享变量,多个线程可以对其进行增加一,如果不使用同步的话,由于递增操作是获取 -> 加1 -> 保存三步操作,所以可能导致导致计数不准确,如下表:

假如当前 count=0,t1 时刻线程 A 读取了 count 值到本地变量 countA。

然后 t2 时刻递增 countA 值为1,同时线程 B 读取 count 的值0放到本地变量 countB 值为0(因为 countA 还没有写入主内存)。

t3 时刻线程 A 才把 countA 为1的值写入主内存,至此线程 A 一次计数完毕,同时线程 B 递增 CountB 值为1。

t4 时刻线程 B 把 countB 值1写入内存,至此线程 B 一次计数完毕。

先不考虑内存可见性问题,明明是两次计数哇,为啥最后结果还是1而不是2呢?其实这就是共享变量的线程安全问题。

那么如何解决?这就需要在线程访问共享变量时候进行适当的同步,Java 中首屈一指的是使用关键字 Synchronized 进行同步,这个下面会有具体介绍。

共享变量的内存可见性问题

要谈内存可见性首先需要介绍下 Java 中多线程下处理共享变量时候的内存模型。

Java 编程之美:并发编程_第4张图片


如上图,Java 内存模型规定了所有的变量都存放在主内存中,当线程使用变量时候都是把主内存里面的变量拷贝到了自己的工作空间或者叫做工作内存。

Java 内存模型是个抽象的概念,那么在实际实现中什么是线程的工作内存呢?

Java 编程之美:并发编程_第5张图片

如上图是双核 CPU 系统架构,每核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算,并且有自己的一级缓存,并且有些架构里面双核还有个共享的二级缓存。

那么 对应 Java 内存模型里面的工作内存,在实现上这里是指 L1 或者 L2 缓存或者 CPU 的寄存器。

假如线程 A 和 B 同时去处理一个共享变量,会出现什么情况呢?

使用上图 CPU 架构,假设线程  A和 B 使用不同 CPU 进行去修改共享变量 X,假设 X 的初始化为0,并且当前两级 Cache 都为空的情况,具体看下面分析:

  • 假设线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以到主内存加载了 X=0,然后会把 X=0 的值缓存到两级缓存,假设线程 A 修改 X 的值为1,然后写入到两级 Cache,并且刷新到主内存(注:如果没刷新会主内存也会存在内存不可见问题)。

    这时候线程 A 所在的 CPU 的两级 Cache 内和主内存里面 X 的值都是1;

  • 然后假设线程 B 这时候获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X=1;然后线程 B 修改 X 的值为2;然后存放到线程2所在的一级 Cache 和共享二级 Cache,最后更新主内存值为2;

  • 然后假设线程 A 这次又需要修改 X 的值,获取时候一级缓存命中获取 X=1,到这里问题就出现了,明明线程 B 已经把 X 的值修改为了2,为啥线程 A 获取的还是1呢?

    这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。

那么对于共享变量内存不可见问题如何解决呢?Java 中首屈一指的 Synchronized 和 Volatile 关键字就可以解决这个问题,下面会有讲解。

Java 中 Synchronized 关键字

Synchronized 块是 Java 提供的一种原子性内置锁,Java 中每个对象都可以当做一个同步锁的功能来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。

线程在进入 Synchronized 代码块前会自动尝试获取内部锁,如果这时候内部锁没有被其他线程占有,则当前线程就获取到了内部锁,这时候其它企图访问该代码块的线程会被阻塞挂起。

拿到内部锁的线程会在正常退出同步代码块或者异常抛出后或者同步块内调用了该内置锁资源的 wait 系列方法时候释放该内置锁;

内置锁是排它锁,也就是当一个线程获取这个锁后,其它线程必须等待该线程释放锁才能获取该锁。

上一节讲了多线程并发修改共享变量时候会存在内存不可见的问题,究其原因是因为 Java 内存模型中线程操作共享变量时候会从自己的工作内存中获取而不是从主内存获取或者线程写入到本地内存的变量没有被刷新会主内存。

下面讲解下 Synchronized 的一个内存语义,这个内存语义就可以解决共享变量内存不可见性问题。

线程进入 Synchronized 块的语义是会把在 Synchronized 块内使用到的变量从线程的工作内存中清除,在 Synchronized 块内使用该变量时候就不会从线程的工作内存中获取了,而是直接从主内存中获取;

退出 Synchronized 块的内存语义是会把 Synchronized 块内对共享变量的修改刷新到主内存。

对应上面一节讲解的假如线程在 Synchronized 块内获取变量 X 的值,那么线程首先会清空所在的 CPU 的缓存,然后从主内存获取变量 X 的值;

当线程修改了变量的值后会把修改的值刷新回主内存。

其实这也是加锁和释放锁的语义,当获取锁后会清空本地内存中后面将会用到的共享变量,在使用这些共享变量的时候会从主内存进行加载;

在释放锁时候会刷新本地内存中修改的共享变量到主内存。

除了可以解决共享变量内存可见性问题外,Synchronized 经常被用来实现原子性操作,另外注意,Synchronized 关键字会引起线程上下文切换和线程调度的开销。

Java 中 Volatile 关键字

上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太重,因为它会引起线程上下文的切换开销,对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用了 volatile 关键字。

一旦一个变量被 volatile 修饰了,当线程获取这个变量值的时候会首先清空线程工作内存中该变量的值,然后从主内存获取该变量的值;

当线程写入被 volatile 修饰的变量的值的时候,首先会把修改后的值写入工作内存,然后会刷新到主内存。这就保证了对一个变量的更新对其它线程马上可见。

下面看一个使用 volatile 关键字解决内存不可见性的一个例子,如下代码的共享变量 value 是线程不安全的,因为它没有进行适当同步措施。

    public class ThreadNotSafeInteger {        private int value;        public int get() {            return value;
        }        public void set(int value) {            this.value = value;
        }
    }

首先看下使用 synchronized 关键字进行同步方式如下:

    public class ThreadSafeInteger {        private int value;        public synchronized int get() {            return value;
        }        public synchronized  void set(int value) {            this.value = value;
        }
    }

然后看下使用 volatile 进行同步如下:

    public class ThreadSafeInteger {        private volatile int value;        public int get() {            return value;
        }        public void set(int value) {            this.value = value;
        }
    }

这里使用 synchronized 和使用 volatile 是等价的,都解决了共享变量 value 的内存不可见性问题;但是前者是独占锁,同时只能有一个线程调用 get() 方法,其它调用线程会被阻塞;

并且会存在线程上下文切换和线程重新调度的开销;而后者是非阻塞算法,不会造成线程上下文切换的开销。

这里使用 synchronized 和使用 volatile 是等价的,但是并不是所有情况下都是等价的,这是因为 volatile 虽然提供了可见性保证,但是并没有保证操作的原子性。

  • 什么是线程?线程和进程的关系。

  • 线程创建与运行。创建一个线程有那几种方式?有何区别?

  • 线程通知与等待,多线程同步的基础设施。

  • 线程的虚假唤醒,以及如何避免。

  • 等待线程执行终止的 join 方法。想让主线程在子线程执行完毕后在做一点事情?

  • 让线程睡眠的 sleep 方法,sleep 的线程会释放持有的锁?

  • 线程中断。中断一个线程,被中断的线程会自己终止?

  • 理解线程上下文切换。线程多了一定好?

  • 线程死锁,以及如何避免。

  • 守护线程与用户线程。当 main 函数执行完毕,但是还有用户线程存在的时候,JVM 进程会退出?

什么是线程

在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程至少有一个线程,进程中的多个线程是共享进程的资源的。

操作系统在分配资源时候是把资源分配给进程的,但是 CPU 资源就比较特殊,它是分派到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。

Java 中当我们启动 main 函数时候其实就启动了一个 JVM 的进程,而 main 函数所在线程就是这个进程中的一个线程,也叫做主线程。

Java 编程之美:并发编程_第6张图片

如图一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器,栈区域。

其中程序计数器是一块内存区域,用来记录线程当前要执行的指令地址,那么程序计数器为何要设计为线程私有的呢?

前面说了线程是占用 CPU 执行的基本单位,而 CPU 一般是使用时间片轮转方式让线程轮询占用的,所以当前线程 CPU 时间片用完后,要让出 CPU,等下次轮到自己时候在执行,那么如何知道之前程序执行到哪里了?

其实程序计数器就是为了记录该线程让出 CPU 时候的执行地址,待再次分配到时间片时候就可以从自己私有的计数器指定地址继续执行了。

另外每个线程有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其它线程是访问不了的,另外栈还用来存放线程的调用栈帧。

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时候分配的,堆里面主要存放使用 new 操作创建的对象实例。

方法区则是用来存放进程中的代码片段的,是线程共享的。

线程创建与运行

Java 中有三种线程创建方法,分别为实现 Runnable 接口的run方法、继承 Thread 类并重写 run 方法、使用 FutureTask 方式。

首先看下继承 Thread 方法的实现:

public class ThreadTest {    //继承Thread类并重写run方法
    public static class MyThread extends Thread {        @Override
        public void run() {

            System.out.println("I am a child thread");

        }
    }    public static void main(String[] args) {        // 创建线程
        MyThread thread = new MyThread();        // 启动线程
        thread.start();
    }
}

如上代码 MyThread 类继承了 Thread 类,并重写了 run 方法,然后调用了线程的 start 方法启动了线程,当创建完 thread 对象后该线程并没有被启动执行.

当调用了 start 方法后才是真正启动了线程。其实当调用了 start 方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除 CPU 资源外的其它资源,等获取 CPU 资源后才会真正处于运行状态。

当 run 方法执行完毕,该线程就处于终止状态了。使用继承方式好处是 run 方法内获取当前线程直接使用 this 就可以,无须使用 Thread.currentThread() 方法,不好的地方是 Java 不支持多继承,如果继承了 Thread 类那么就不能再继承其它类。

另外任务与代码没有分离,当多个线程执行一样的任务时候需要多份任务代码,而 Runable 则没有这个限制,下面看下实现 Runnable 接口的 run 方法方式:

    public static class RunableTask implements Runnable{        @Override
        public void run() {
            System.out.println("I am a child thread");
        }

    } public static void main(String[] args) throws InterruptedException{

        RunableTask task = new RunableTask();        new Thread(task).start();        new Thread(task).start();
}

如上面代码,两个线程公用一个 task 代码逻辑,需要的话 RunableTask 可以添加参数进行任务区分,另外 RunableTask 可以继承其他类,但是上面两种方法都有一个缺点就是任务没有返回值,下面看最后一种是使用 FutureTask:

//创任务类,类似Runablepublic static class CallerTask implements Callable<String>{        @Override
        public String call() throws Exception {            return "hello";
        }

    }    public static void main(String[] args) throws InterruptedException {    // 创建异步任务
        FutureTask futureTask  = new FutureTask<>(new CallerTask());        //启动线程
        new Thread(futureTask).start();        try {           //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
}

注:每种方式都有自己的优缺点,应该根据实际场景进行选择。

线程通知与等待

Java 中 Object 类是所有类的父类,鉴于继承机制,Java 把所有类都需要的方法放到了 Object 类里面,其中就包含本节要讲的通知等待系列函数,这些通知等待函数是组成并发包中线程同步组件的基础。

下面讲解下 Object 中关于线程同步的通知等待函数。

void wait() 方法

首先谈下什么是共享资源,所谓共享资源是说该资源被多个线程共享,多个线程都可以去访问或者修改的资源。另外本文当讲到的共享对象就是共享资源。

当一个线程调用一个共享对象的 wait() 方法时候,调用线程会被阻塞挂起,直到下面几个事情之一发生才返回:

  1. 其它线程调用了该共享对象的 notify() 或者 notifyAll() 方法;

  2. 其它线程调用了该线程的 interrupt() 方法设置了该线程的中断标志,该线程会抛出 InterruptedException 异常返回。

另外需要注意的是如果调用 wait() 方法的线程没有事先获取到该对象的监视器锁,则调用 wait() 方法时候调用线程会抛出 IllegalMonitorStateException 异常。

那么一个线程如何获取到一个共享变量的监视器那?

(1)执行使用 synchronized 同步代码块时候,使用该共享变量作为参数:

synchronized(共享变量){       //doSomething
   }

(2)调用该共享变量的方法,并且该方法使用了 synchronized 修饰:

synchronized void add(int a,int b){       //doSomething}

另外需要注意的是一个线程可以从挂起状态变为可以运行状态(也就是被唤醒)即使该线程没有被其它线程调用 notify(),notifyAll() 进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但是还是需要防范于未然的,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中去调用 wait() 方法进行防范,退出循环的条件是条件满足了唤醒该线程。


    synchronized (obj) {             while (条件不满足){
               obj.wait();  
             }
    }

如上代码是经典的调用共享变量 wait() 方法的实例,首先通过同步块获取 obj 上面的监视器锁,然后通过 while 循环内调用 obj 的 wait() 方法。

下面从生产者消费者例子来加深理解,如下面代码是一个生产者的例子,其中 queue 为共享变量,生产者线程在调用 queue 的 wait 方法前,通过使用 synchronized 关键字拿到了该共享变量 queue 的监视器。

所以调用 wait() 方法才不会抛出 IllegalMonitorStateException 异常,如果当前队列没有空闲容量则会调用 queued 的 wait() 挂起当前线程,这里使用循环就是为了避免上面说的虚假唤醒问题。

假如当前线程虚假唤醒了,但是队列还是没有空余容量的话,当前线程还是会调用 wait() 把自己挂起。

//生产线程synchronized (queue) { 

    //消费队列满,则等待队列空闲
    while (queue.size() == MAX_SIZE) { 
        try { 
            //挂起当前线程,并释放通过同步块获取的queue上面的锁,让消费线程可以获取该锁,然后获取队列里面元素
            queue.wait(); 
        } catch (Exception ex) { 
            ex.printStackTrace(); 
        } 
    }    //空闲则生成元素,并通知消费线程
    queue.add(ele); 
    queue.notifyAll(); 

    } 
}
//消费线程synchronized (queue) { 

    //消费队列为空
    while (queue.size() == 0) { 
        try
            //挂起当前线程,并释放通过同步块获取的queue上面的锁,让生产线程可以获取该锁,生产元素放入队列
            queue.wait(); 
        } catch (Exception ex) { 
            ex.printStackTrace(); 
        } 
    }    //消费元素,并通知唤醒生产线程
    queue.take(); 
    queue.notifyAll(); 

    } 
}

另外当一个线程调用了共享变量的 wait() 方法后该线程会被挂起,同时该线程会暂时释放对该共享变量监视器的持有,直到另外一个线程调用了共享变量的 notify() 或者 notifyAll() 方法才有可能会重新获取到该共享变量的监视器的持有权(这里说有可能,是因为考虑到多个线程第一次都调用了 wait() 方法,所以多个线程会竞争持有该共享变量的监视器)

借用上面这个例子来讲解下调用共享变量 wait() 方法后当前线程会释放持有的共享变量的锁的理解。

如上代码假如生产线程 A 首先通过 synchronized 获取到了 queue 上的锁,那么其它生产线程和所有消费线程都会被阻塞,线程 A 获取锁后发现当前队列已满会调用 queue.wait() 方法阻塞自己,然后会释放获取的 queue 上面的锁,这里考虑下为何要释放该锁?

如果不释放,由于其它生产线程和所有消费线程已经被阻塞挂起,而线程 A 也被挂起,这就处于了死锁状态。

这里线程 A 挂起自己后释放共享变量上面的锁就是为了打破死锁必要条件之一的持有并等待原则。

关于死锁下面章节会有讲到,线程 A 释放锁后其它生产线程和所有消费线程中会有一个线程获取 queue 上的锁进而进入同步块,这就打破了死锁。

最后再举一个例子说明当一个线程调用共享对象的 wait() 方法被阻塞挂起后,如果其它线程中断了该线程,则该线程会抛出 InterruptedException 异常后返回:

public class WaitNotifyInterupt {    static Object obj = new Object();    public static void main(String[] args) throws InterruptedException {        //创建线程
        Thread threadA = new Thread(new Runnable() {            public void run() {                try {
                    System.out.println("---begin---");                    //阻塞当前线程
                    obj.wait();
                    System.out.println("---end---");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();

        Thread.sleep(1000);

        System.out.println("---begin interrupt threadA---");
        threadA.interrupt();
        System.out.println("---end interrupt threadA---");
    }
}

运行上面代码输出为:

如上代码 threadA 调用了共享对 obj 的 wait() 方法后阻塞挂起了自己,然后主线程在休眠1s后中断了 threadA 线程,可知中断后 threadA 在 obj.wait() 处抛出了 java.lang.IllegalMonitorStateException 异常后返回后终止。

void wait(long timeout) 方法

该方法相比 wait() 方法多一个超时参数,不同在于如果一个线程调用了共享对象的该方法挂起后,如果没有在指定的 timeout ms 时间内被其它线程调用该共享变量的 notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。

需要注意的是如果在调用该函数时候 timeout 传递了负数会抛出 IllegalArgumentException 异常。

void wait(long timeout, int nanos) 方法

内部是调用 wait(long timeout),如下代码:只是当 nanos>0 时候让参数一递增1。

 public final void wait(long timeout, int nanos) throws InterruptedException {        if (timeout < 0) {            throw new IllegalArgumentException("timeout value is negative");
        }        if (nanos < 0 || nanos > 999999) {            throw new IllegalArgumentException(                                "nanosecond timeout value out of range");
        }        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

void notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程,一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。

另外被唤醒的线程不能马上从 wait 返回继续执行,它必须获取了共享对象的监视器后才可以返回,也就是唤醒它的线程释放了共享变量上面的监视器锁后,被唤醒它的线程也不一定会获取到共享对象的监视器,这是因为该线程还需要和其它线程一块竞争该锁,只有该线程竞争到了该共享变量的监视器后才可以继续执行。

类似 wait 系列方法,只有当前线程已经获取到了该共享变量的监视器锁后,才可以调用该共享变量的 notify() 方法,否者会抛出 IllegalMonitorStateException 异常。

void notifyAll() 方法

不同于 nofity() 方法在共享变量上调用一次就会唤醒在该共享变量上调用 wait 系列方法被挂起的一个线程,notifyAll() 则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

最后本小节最后讲一个例子来说明 notify() 和 notifyAll() 的具体含义和一些需要注意的地方,代码实例如下:

private static volatile Object resourceA = new Object();public static void main(String[] args) throws InterruptedException {    // 创建线程
    Thread threadA = new Thread(new Runnable() {        public void run() {            // 获取resourceA共享资源的监视器锁
            synchronized (resourceA) {

                System.out.println("threadA get resourceA lock");                try {

                    System.out.println("threadA begin wait");
                    resourceA.wait();
                    System.out.println("threadA end wait");

                } catch (InterruptedException e) {                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    });    // 创建线程
    Thread threadB = new Thread(new Runnable() {        public void run() {            synchronized (resourceA) {
                System.out.println("threadB get resourceA lock");                try {

                    System.out.println("threadB begin wait");
                    resourceA.wait();
                    System.out.println("threadB end wait");

                } catch (InterruptedException e) {                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    });    // 创建线程
    Thread threadC = new Thread(new Runnable() {        public void run() {            synchronized (resourceA) {

                System.out.println("threadC begin notify");
                resourceA.notifyAll();
            }
        }
    });    // 启动线程
    threadA.start();
    threadB.start();

    Thread.sleep(1000);
    threadC.start();    // 等待线程结束
    threadA.join();
    threadB.join();
    threadC.join();
    System.out.println("main over");
}

输出结果:

如上代码开启了三个线程,其中线程 A 和 B 分别调用了共享资源 resourceA 的 wait() 方法,线程 C 则调用了 nofity() 方法。

这里启动线程 C 前首先调用 sleep 方法让主线程休眠 1s,目的是让线程 A 和 B 全部执行到调用 wait 方法后在调用线程 C 的 notify 方法。

这个例子企图希望在线程 A 和线程 B 都因调用共享资源 resourceA 的 wait() 方法而被阻塞后,线程 C 在调用 resourceA 的 notify() 方法,希望可以唤醒线程 A 和线程 B,但是从执行结果看只有一个线程 A 被唤醒了,线程 B 没有被唤醒,

从结果看线程调度器这次先调度了线程 A 占用 CPU 来运行,线程 A 首先获取 resourceA 上面的锁,然后调用 resourceA 的 wait() 方法挂起当前线程并释放获取到的锁,然后线程 B 获取到 resourceA 上面的锁并调用了 resourceA 的 wait(),此时线程 B 也被阻塞挂起并释放了 resourceA 上的锁。

线程 C 休眠结束后在共享资源 resourceA 上调用了 notify() 方法,则会激活 resourceA 的阻塞集合里面的一个线程,这里激活了线程 A,所以线程 A 调用的 wait() 方法返回了,线程 A 执行完毕。而线程 B 还处于阻塞状态。

如果把线程 C 里面调用的 notify() 改为调用 notifyAll() 而执行结果如下:

可知线程 A 和线程 B 被挂起后,线程 C 调用 notifyAll() 函数会唤醒在 resourceA 等待的所有线程,这里线程 A 和线程 B 都会被唤醒,只是线程 B 先获取到 resourceA 上面的锁然后从 wait() 方法返回。

等线程 B 执行完毕后,线程 A 又获取了 resourceA 上面的锁,然后从 wait() 方返回,当线程 A 执行完毕,主线程就返回后,然后打印输出。

注:在调用具体共享对象的 wait 或者 notify 系列函数前要先获取共享对象的锁;另外通知和等待是实现线程同步的原生方法,理解它们的协作功能很有必要;最后由于线程虚假唤醒的存在,一定要使用循环检查的方式。

等待线程执行终止的 join 方法

在项目实践时候经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行。

比如多个线程去加载资源,当多个线程全部加载完毕后在汇总处理,Thread 类中有个静态的 join 方法就可以做这个事情,前面介绍的等待通知方法是属于 Object 类的,而 join 方法则是直接在 Thread 类里面提供的,join 是无参,返回值为 void 的方法。下面看一个简单的例子来介绍 join 的使用:

public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {            @Override
            public void run() {                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("child threadOne over!");

            }
        });

        Thread threadTwo = new Thread(new Runnable() {            @Override
            public void run() {                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("child threadTwo over!");

            }
        });        //启动子线程
        threadOne.start();
        threadTwo.start();

        System.out.println("wait all child thread over!");        //等待子线程执行完毕,返回
        threadOne.join();
        threadTwo.join();

        System.out.println("all child thread over!");

    }

如代码主线程里面启动了两个子线程,然后在分别调用了它们的 join() 方法,那么主线程首先会阻塞到 threadOne.join() 方法,等 threadOne 执行完毕后返回,threadOne 执行完毕后 threadOne.join() 就会返回。

然后主线程调用 threadTwo.join() 后再次被阻塞,等 threadTwo 执行完毕后主线程也就返回了。

这里只是为了演示 join 的作用,对应这类需求后面会讲的 CountDownLatch 是不错选择。

  • ThreadLocal 的实现原理,ThreadLocal 作为变量的线程隔离方式,其内部是如何做的?

  • InheritableThreadLocal 的实现原理,InheritableThreadLocal 是如何弥补 ThreadLocal 不支持继承的特性?

  • JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadLocal 的原理来解决 Random 的局限性?

  • 最后 ThreadLocal 的一个使用场景,Spring 框架中 Scope 作用域 Bean 的实现原理。

1. ThreadLocal 的实现原理

多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候,为了保证线程安全,一般需要使用者在访问共享变量的时候进行适当的同步,如下图:

Java 编程之美:并发编程_第7张图片

同步的措施一般是加锁,这就需要使用者对锁也要有一定了解,这显然加重了使用者的负担。

那么有没有一个方式当创建一个变量时候,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实 ThreadLocal 就可以做这个事情,虽然 ThreadLocal 的出现并不是为了解决上面的问题而出现的。

ThreadLocal 是在 JDK 包里面提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作的自己本地内存里面的变量,从而避免了线程安全问题,创建一个 ThreadLocal 变量后每个线程会拷贝一个变量到自己本地内存,如下图:

1.1 ThreadLocal 简单使用

本节来看下 ThreadLocal 如何使用,从而加深理解,本例子开启了两个线程,每个线程内部设置了本地变量的值,然后调用 print 函数打印当前本地变量的值,如果打印后调用了本地变量的 remove 方法则会删除本地内存中的该变量,代码如下:

public class ThreadLocalTest {    //(1)打印函数
    static void print(String str){        //1.1  打印当前线程本地内存中localVariable变量的值
        System.out.println(str + ":" +localVariable.get());        //1.2 清除当前线程本地内存中localVariable变量
        //localVariable.remove();
    }    //(2) 创建ThreadLocal变量
    static ThreadLocal localVariable = new ThreadLocal<>();    public static void main(String[] args) {        //(3) 创建线程one
        Thread threadOne = new Thread(new  Runnable() {            public void run() {                //3.1 设置线程one中本地变量localVariable的值
                localVariable.set("threadOne local variable");                //3.2 调用打印函数
                print("threadOne");                //3.3打印本地变量值
                System.out.println("threadOne remove after" + ":" +localVariable.get());

            }
        });        //(4) 创建线程two
        Thread threadTwo = new Thread(new  Runnable() {            public void run() {                //4.1 设置线程one中本地变量localVariable的值
                localVariable.set("threadTwo local variable");                //4.2 调用打印函数
                print("threadTwo");                //4.3打印本地变量值
                System.out.println("threadTwo remove after" + ":" +localVariable.get());

            }
        });        //(5)启动线程
        threadOne.start();
        threadTwo.start();
    }

运行结果:

threadOne:threadOne local variable
threadTwo:threadTwo local variable
threadOne remove after:threadOne local variable
threadTwo remove after:threadTwo local variable
  • 代码(2)创建了一个 ThreadLocal 变量;

  • 代码(3)、(4)分别创建了线程 one 和 two;

  • 代码(5)启动了两个线程;

  • 线程 one 中代码 3.1 通过 set 方法设置了 localVariable 的值,这个设置的其实是线程 one 本地内存中的一个拷贝,这个拷贝线程 two 是访问不了的。然后代码 3.2 调用了 print 函数,代码 1.1 通过 get 函数获取了当前线程(线程 one)本地内存中 localVariable 的值;

  • 线程 two 执行类似线程 one。

解开代码 1.2 的注释后,再次运行,运行结果为:

threadOne:threadOne local variable
threadOne remove after:nullthreadTwo:threadTwo local variable
threadTwo remove after:null

1.2 ThreadLocal 实现原理

首先看下 ThreadLocal 相关的类的类图结构。


如上类图可知 Thread 类中有一个 threadLocals 和 inheritableThreadLocals 都是 ThreadLocalMap 类型的变量,而 ThreadLocalMap 是一个定制化的 Hashmap,默认每个线程中这个两个变量都为 null,只有当前线程第一次调用了 ThreadLocal 的 set 或者 get 方法时候才会进行创建。

其实每个线程的本地变量不是存放到 ThreadLocal 实例里面的,而是存放到调用线程的 threadLocals 变量里面。也就是说 ThreadLocal 类型的本地变量是存放到具体的线程内存空间的。

ThreadLocal 就是一个工具壳,它通过 set 方法把 value 值放入调用线程的 threadLocals 里面存放起来,当调用线程调用它的 get 方法时候再从当前线程的 threadLocals变 量里面拿出来使用。

如果调用线程一直不终止,那么这个本地变量会一直存放到调用线程的 threadLocals 变量里面,所以当不需要使用本地变量时候可以通过调用 ThreadLocal 变量的 remove 方法,从当前线程的 threadLocals 里面删除该本地变量。

另外 Thread 里面的 threadLocals 为何设计为 map 结构呢?很明显是因为每个线程里面可以关联多个 ThreadLocal 变量。

下面简单分析下 ThreadLocal 的 set,get,remove 方法的实现逻辑:

  • void set(T value)

    public void set(T value) {        //(1)获取当前线程
        Thread t = Thread.currentThread();        //(2)当前线程作为key,去查找对应的线程变量,找到则设置
        ThreadLocalMap map = getMap(t);        if (map != null)
            map.set(this, value);        else
        //(3)第一次调用则创建当前线程对应的HashMap
            createMap(t, value);
    }

如上代码(1)首先获取调用线程,然后使用当前线程作为参数调用了 getMap(t) 方法,getMap(Thread t) 代码如下:

 ThreadLocalMap getMap(Thread t) {        return t.threadLocals;
    }

可知 getMap(t) 所做的就是获取线程自己的变量 threadLocals,threadlocal 变量是绑定到了线程的成员变量里面。

如果 getMap(t) 返回不为空,则把 value 值设置进入到 threadLocals,也就是把当前变量值放入了当前线程的内存变量 threadLocals,threadLocals 是个 HashMap 结构,其中 key 就是当前 ThreadLocal 的实例对象引用,value 是通过 set 方法传递的值。

如果 getMap(t) 返回空那说明是第一次调用 set 方法,则创建当前线程的 threadLocals 变量,下面看 createMap(t, value) 里面做了啥呢?

     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

可知就是创建当前线程的 threadLocals 变量。

  • T get()

    public T get() {        //(4) 获取当前线程
        Thread t = Thread.currentThread();        //(5)获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);        //(6)如果threadLocals不为null,则返回对应本地变量值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")
                T result = (T)e.value;                return result;
            }
        }        //(7)threadLocals为空则初始化当前线程的threadLocals成员变量
                return setInitialValue();
    }

如上代码(4)首先获取当前线程实例,如果当前线程的 threadLocals 变量不为 null 则直接返回当前线程绑定的本地变量。否者执行代码(7)进行初始化,setInitialValue() 的代码如下:

    private T setInitialValue() {        //(8)初始化为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);        //(9)如果当前线程的threadLocals变量不为空
        if (map != null)
            map.set(this, value);        else
        //(10)如果当前线程的threadLocals变量为空
            createMap(t, value);        return value;
    }
    protected T initialValue() {        return null;
    }

如上代码如果当前线程的 threadLocals 变量不为空,则设置当前线程的本地变量值为 null,否者调用 createMap 创建当前线程的 createMap 变量。

  • void remove()

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());         if (m != null)
             m.remove(this);
     }

如上代码,如果当前线程的 threadLocals 变量不为空,则删除当前线程中指定 ThreadLocal 实例的本地变量。

注:每个线程内部都有一个名字为 threadLocals 的成员变量,该变量类型为 HashMap,其中 key 为我们定义的 ThreadLocal 变量的 this 引用,value 则为我们 set 时候的值,每个线程的本地变量是存到线程自己的内存变量 threadLocals 里面的,如果当前线程一直不消失那么这些本地变量会一直存到,所以可能会造成内存泄露,所以使用完毕后要记得调用 ThreadLocal 的 remove 方法删除对应线程的 threadLocals 中的本地变量。

1.3 子线程中获取不到父线程中设置的 ThreadLocal 变量的值

首先看个例子说明标题的意思:

public class TestThreadLocal {    //(1) 创建线程变量
    public static ThreadLocal threadLocal = new ThreadLocal();    public static void main(String[] args) {        //(2)  设置线程变量
        threadLocal.set("hello world");        //(3) 启动子线程
        Thread thread = new Thread(new  Runnable() {            public void run() {                //(4)子线程输出线程变量的值
                System.out.println("thread:" + threadLocal.get());

            }
        });
        thread.start();        //(5)主线程输出线程变量值
        System.out.println("main:" + threadLocal.get());

    }
}

结果为:

main:hello world
thread:null

也就是说同一个 ThreadLocal 变量在父线程中设置值后,在子线程中是获取不到的。

根据上节的介绍,这个应该是正常现象,因为子线程调用 get 方法时候当前线程为子线程,而调用 set 方法设置线程变量是 main 线程,两者是不同的线程,自然子线程访问时候返回 null,那么有办法让子线程访问到父线程中的值吗?答案是有。

2. InheritableThreadLocal 原理

为了解决上节的问题 InheritableThreadLocal 应运而生,InheritableThreadLocal 继承自 ThreadLocal,提供了一个特性,就是子线程可以访问到父线程中设置的本地变量。

下面看下 InheritableThreadLocal 的代码:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {    //(1)
    protected T childValue(T parentValue) {        return parentValue;
    }    //(2)
    ThreadLocalMap getMap(Thread t) {       return t.inheritableThreadLocals;
    }    //(3)
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

如上代码可知 InheritableThreadLocal 继承了 ThreadLocal,并重写了三个方法。

  • 代码(3)可知 InheritableThreadLocal 重写了 createMap 方法,那么可知现在当第一次调用 set 方法时候创建的是当前线程的 inheritableThreadLocals 变量的实例而不再是 threadLocals。

  • 代码(2)可知当调用 get 方法获取当前线程的内部 map 变量时候,获取的是 inheritableThreadLocals 而不再是 threadLocals。

综上可知在 InheritableThreadLocal 的世界里,线程中的变量 inheritableThreadLocals 替代了 threadLocals。

  • 下面我们看下重写的代码(1)是何时被执行,以及如何实现的子线程可以访问父线程本地变量的。这个要从 Thread 创建的代码看起,Thread 的默认构造函数及 Thread.java 类的构造函数如下:

         public Thread(Runnable target) {
          init(null, target, "Thread-" + nextThreadNum(), 0);
      }
      private void init(ThreadGroup g, Runnable target, String name,                      long stackSize, AccessControlContext acc) {
            ...            //(4)获取当前线程
            Thread parent = currentThread();
            ...            //(5)如果父线程的inheritableThreadLocals变量不为null
            if (parent.inheritableThreadLocals != null)            //(6)设置子线程中的inheritableThreadLocals变量
            this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);        this.stackSize = stackSize;
        tid = nextThreadID();
    }

创建线程时候在构造函数里面会调用 init 方法,前面讲到了 inheritableThreadLocal 类 get,set 方法操作的是变量 inheritableThreadLocals,所以这里 inheritableThreadLocal 变量就不为 null,所以会执行代码(6),下面看下 createInheritedMap 代码:

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {        return new ThreadLocalMap(parentMap);
    }

可知 createInheritedMap 内部使用父线程的 inheritableThreadLocals 变量作为构造函数创建了一个新的 ThreadLocalMap 变量。

然后赋值给了子线程的 inheritableThreadLocals 变量,那么下面看看 ThreadLocalMap 的构造函数里面做了什么:

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];                if (e != null) {                    @SuppressWarnings("unchecked")
                    ThreadLocal key = (ThreadLocal) e.get();                    if (key != null) {                        //(7)调用重写的方法
                        Object value = key.childValue(e.value);//返回e.value
                        Entry c = new Entry(key, value);                        int h = key.threadLocalHashCode & (len - 1);                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        } 
  

如上代码所做的事情就是把父线程的 inheritableThreadLocals 成员变量的值复制到新的 ThreadLocalMap 对象,其中代码(7)InheritableThreadLocal 类重写的代码(1)也映入眼帘了。

总结:InheritableThreadLocal 类通过重写代码(2)和(3)让本地变量保存到了具体线程的 inheritableThreadLocals 变量里面,线程通过 InheritableThreadLocal 类实例的 set 或者 get 方法设置变量时候就会创建当前线程的 inheritableThreadLocals 变量。

当父线程创建子线程时候,构造函数里面会把父线程中 inheritableThreadLocals 变量里面的本地变量拷贝一份复制到子线程的 inheritableThreadLocals 变量里面。

把上节代码(1)修改为:

    //(1) 创建线程变量
    public static ThreadLocal threadLocal = new InheritableThreadLocal();

运行结果为:

thread:hello world
main:hello world

可知现在可以从子线程中正常的获取到线程变量值了。

那么什么情况下需要子线程可以获取到父线程的 threadlocal 变量呢,情况还是蛮多的,比如存放用户登录信息的 threadlocal 变量,很有可能子线程中也需要使用用户登录信息,再比如一些中间件需要用统一的追踪 ID 把整个调用链路记录下来的情景。

3. JDK 并发包中 ThreadLocalRandom 类原理剖析

ThreadLocalRandom 类是 JDK7 在 JUC 包下新增的随机数生成器,它解决了 Random 类在多线程下的不足。本节就来讲解下 JUC 下为何新增该类,以及该类的实现原理。

3.1 Random 类及其局限性

在 JDK7 之前包括现在,java.util.Random 应该是使用比较广泛的随机数生成工具类,另外 java.lang.Math 中的随机数生成也是使用的 java.util.Random 的实例。下面先看看 java.util.Random 的使用:

public class RandomTest {    public static void main(String[] args) {        //(1)创建一个默认种子的随机数生成器
        Random random = new Random();        //(2)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}
  • 代码(1)创建一个默认随机数生成器,使用默认的种子。

  • 代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。

这里提下随机数的生成需要一个默认的种子,这个种子其实是一个 long 类型的数字,这个种子要么在 Random 的时候通过构造函数指定,那么默认构造函数内部会生成一个默认的值,有了默认的种子后,如何生成随机数呢?

    public int nextInt(int bound) {        //(3)参数检查
        if (bound <= 0)            throw new IllegalArgumentException(BadBound);        //(4)根据老的种子生成新的种子
        int r = next(31);        //(5)根据新的种子计算随机数
        ...        return r;
    }

如上代码可知新的随机数的生成需要两个步骤:

  • 首先需要根据老的种子生成新的种子。

  • 然后根据新的种子来计算新的随机数。

其中步骤(4)我们可以抽象为 seed=f(seed),其中 f 是一个固定的函数,比如 seed= f(seed)=a*seed+b;,步骤(5)也可以抽象为 g(seed,bound),其中 g 是一个固定的函数,比如 g(seed,bound)=(int)((bound * (long)seed) >> 31);

在单线程情况下每次调用 nextInt 都是根据老的种子计算出来新的种子,这是可以保证随机数产生的随机性的。

但是在多线程下多个线程可能都拿同一个老的种子去执行步骤(4)计算新的种子,这会导致多个线程产生的新种子是一样的,由于步骤(5)算法是固定的,所以会导致多个线程产生相同的随机值,这并不是我们想要的。

所以需要保证步骤(4)的原子性,也就是说多个线程在根据同一个老种子计算新种子时候,第一个线程的新种子计算出来后,第二个线程要丢弃自己老的种子,要使用第一个线程的新种子来计算自己的新种子,依次类推,只有保证了这个,才能保证多线程下产生的随机数是随机的。

Random 函数使用一个原子变量达到了这个效果,在创建 Random 对象时候初始化的种子就保存到了种子原子变量里面,下面看下 next() 代码:

    protected int next(int bits) {        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {            //(6)
            oldseed = seed.get();            //(7)
            nextseed = (oldseed * multiplier + addend) & mask;            //(8)
        } while (!seed.compareAndSet(oldseed, nextseed));        //(9)
        return (int)(nextseed >>> (48 - bits));
    }
  • 代码(6)获取当前原子变量种子的值;

  • 代码(7)根据当前种子值计算新的种子;

  • 代码(8)使用 CAS 操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6),那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的 CAS 操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,可见这里解决了上面提到的问题,也就保证了随机数的随机性。

  • 代码(9)则使用固定算法根据新的种子计算随机数。

总结:每个 Random 实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。

多线程下使用单个 Random 实例生成随机数时候,多个线程同时计算新的种子时候会竞争同一个原子变量的更新操作,由于原子变量的更新是 CAS 操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能的,所以 ThreadLocalRandom 应运而生。

3.2 ThreadLocalRandom 类

为了解决多线程高并发下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 类,下面首先看下它如何使用:

public class RandomTest {    public static void main(String[] args) {        //(10)获取一个随机数生成器
        ThreadLocalRandom random =  ThreadLocalRandom.current();        //(11)输出10个在0-5(包含0,不包含5)之间的随机数
        for (int i = 0; i < 10; ++i) {
            System.out.println(random.nextInt(5));
        }
    }
}

如上代码(10)调用 ThreadLocalRandom.current() 来获取当前线程的随机数生成器。

下面来分析下 ThreadLocalRandom 的实现原理。从名字看会让我们联想到《Java 并发编程之美:基础篇》 中讲解的 ThreadLocal,ThreadLocal 的出现就是为了解决多线程下变量的隔离问题,让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝。

实际上 ThreadLocalRandom 的实现也是这个原理,Random 的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,如下图:

那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图 ThreadLocalRandom 原理:

Java 编程之美:并发编程_第8张图片

源码分析

首先看下 ThreadLocalRandom 的类图结构:


Java 编程之美:并发编程_第9张图片

可知 ThreadLocalRandom 继承了 Random 并重写了 nextInt 方法,ThreadLocalRandom 中并没有使用继承自 Random 的原子性种子变量。

ThreadLocalRandom 中并没有具体存放种子,具体的种子是存放到具体的调用线程的 threadLocalRandomSeed 变量里面的,ThreadLocalRandom 类似于 ThreadLocal类 就是个工具类。

当线程调用 ThreadLocalRandom 的 current 方法时候 ThreadLocalRandom 负责初始化调用线程的 threadLocalRandomSeed 变量,也就是初始化种子。

当调用 ThreadLocalRandom 的 nextInt 方法时候,实际上是获取当前线程的 threadLocalRandomSeed 变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的 threadLocalRandomSeed 变量,然后在根据新种子和具体算法计算随机数。

这里需要注意的是 threadLocalRandomSeed 变量就是 Thread 类里面的一个普通 long 变量,并不是原子性变量,其实道理很简单,因为这个变量是线程级别的,根本不需要使用原子性变量,如果还是不理解可以思考下 ThreadLocal 的原理。

其中变量 seeder 和 probeGenerator 是两个原子性变量,在初始化调用线程的种子和探针变量时候用到,每个线程只会使用一次。

另外变量 instance 是个 ThreadLocalRandom 的一个实例,该变量是 static 的,当多线程通过 ThreadLocalRandom 的 current 方法获取 ThreadLocalRandom 的实例时候其实获取的是同一个,但是由于具体的种子是存放到线程里面的,所以 ThreadLocalRandom 的实例里面只是与线程无关的通用算法,所以是线程安全的。

下面看看 ThreadLocalRandom 的主要代码实现逻辑。

  • Unsafe 机制的使用,具体的会在高级篇之二里面讲解。

    private static final sun.misc.Unsafe UNSAFE;    private static final long SEED;    private static final long PROBE;    private static final long SECONDARY;    static {        try {            //获取unsafe实例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class tk = Thread.class;            //获取Thread类里面threadLocalRandomSeed变量在Thread实例里面偏移量
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));            //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));            //获取Thread类里面threadLocalRandomProbe变量在Thread实例里面偏移量,这个值在后面讲解的LongAdder里面会用到
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {            throw new Error(e);
        }
    }
  • ThreadLocalRandom current() 方法:该方法获取 ThreadLocalRandom 实例,并初始化调用线程中 threadLocalRandomSeed 和 threadLocalRandomProbe 变量。

    static final ThreadLocalRandom instance = new ThreadLocalRandom();    public static ThreadLocalRandom current() {        //(12)
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)            //(13)
            localInit();        //(14)
        return instance;
    }
    static final void localInit() {        int p = probeGenerator.addAndGet(PROBE_INCREMENT);        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

如上代码(12)如果当前线程中 threadLocalRandomProbe 变量值为0(默认情况下线程的这个变量为0),说明当前线程第一次调用 ThreadLocalRandom 的 current 方法,那么就需要调用 localInit 方法计算当前线程的初始化种子变量。

这里设计为了延迟初始化,不需要使用随机数功能时候 Thread 类中的种子变量就不需要被初始化,这是一种优化。

代码(13)首先计算根据 probeGenerator 计算当前线程中 threadLocalRandomProbe 的初始化值,然后根据 seeder 计算当前线程的初始化种子,然后把这两个变量设置到当前线程。

代码(14)返回 ThreadLocalRandom 的实例,需要注意的是这个方法是静态方法,多个线程返回的是同一个 ThreadLocalRandom 实例。

  • int nextInt(int bound) 方法:计算当前线程的下一个随机数。

    public int nextInt(int bound) {        //(15)参数校验
        if (bound <= 0)            throw new IllegalArgumentException(BadBound);        //(16) 根据当前线程中种子计算新种子
        int r = mix32(nextSeed());        //(17)根据新种子和bound计算随机数
        int m = bound - 1;        if ((bound & m) == 0) // power of two
            r &= m;        else { // reject over-represented candidates
            for (int u = r >>> 1;
                 u + m - (r = u % bound) < 0;
                 u = mix32(nextSeed()) >>> 1)
                ;
        }        return r;
    }

如上代码逻辑步骤与 Random 相似,我们重点看下 nextSeed() 方法:

    final long nextSeed() {
        Thread t; long r; // 
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);        return r;
    }

如上代码首先使用 r = UNSAFE.getLong(t, SEED) 获取当前线程中 threadLocalRandomSeed 变量的值,然后在种子的基础上累加 GAMMA 值作为新种子,然后使用 UNSAFE 的 putLong 方法把新种子放入当前线程的 threadLocalRandomSeed 变量。

注:本节首先讲解了 Random 的实现原理以及介绍了 Random 在多线程下存在竞争种子原子变量更新操作失败后自旋等待的缺点,从而引出 ThreadLocalRandom 类,ThreadLocalRandom 使用 ThreadLocal 的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。

4. Spring Request Scope 作用域 Bean 中 ThreadLocal 的使用

  • rt.jar 中 Unsafe 类主要函数讲解, Unsafe 类提供了硬件级别的原子操作,可以安全的直接操作内存变量,其在 JUC 源码中被广泛的使用,了解其原理为研究 JUC 源码奠定了基础。

  • rt.jar 中 LockSupport 类主要函数讲解,LockSupport 是个工具类,主要作用是挂起和唤醒线程,是创建锁和其它同步类的基础,了解其原理为研究 JUC 中锁的实现奠定基础。

  • 讲解 JDK8 新增原子操作类 LongAdder 实现原理,并讲解 AtomicLong 的缺点是什么,LongAdder 是如何解决 AtomicLong 的缺点的,LongAdder 和 LongAccumulator 是什么关系?

  • JUC 并发包中并发组件 CopyOnWriteArrayList 的实现原理,CopyOnWriteArrayList 是如何通过写时拷贝实现并发安全的 List?

二、 Unsafe 类探究

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子操作,Unsafe 里面的方法都是 native 方法,通过使用 JNI 的方式来访问本地 C++ 实现库。下面我们看下 Unsafe 提供的几个主要方法以及编程时候如何使用 Unsafe 类做一些事情。

2.1 主要方法介绍

  • long objectFieldOffset(Field field) 方法

作用:返回指定的变量在所属类的内存偏移地址,偏移地址仅仅在该 Unsafe 函数中访问指定字段时候使用。如下代码使用 unsafe 获取AtomicLong 中变量 value 在 AtomicLong 对象中的内存偏移。

static {        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
}
  • int arrayBaseOffset(Class arrayClass) 方法
    获取数组中第一个元素的地址

  • int arrayIndexScale(Class arrayClass) 方法
    获取数组中单个元素占用的字节数

  • boolean  compareAndSwapLong(Object obj, long offset, long expect, long update) 方法
    比较对象 obj 中偏移量为 offset 的变量的值是不是和 expect 相等,相等则使用 update 值更新,然后返回 true,否者返回 false

  • public native long getLongVolatile(Object obj, long offset) 方法
    获取对象 obj 中偏移量为 offset 的变量对应的 volatile 内存语义的值。

  • void putLongVolatile(Object obj, long offset, long value) 方法
    设置 obj 对象中内存偏移为 offset 的 long 型变量的值为 value,支持 volatile 内存语义。

  • void putOrderedLong(Object obj, long offset, long value) 方法
    设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。这是有延迟的 putLongVolatile 方法,并不保证值修改对其它线程立刻可见。变量只有使用 volatile 修饰并且期望被意外修改的时候使用才有用。

  • void park(boolean isAbsolute, long time)
    阻塞当前线程,其中参数 isAbsolute 等于 false 时候,time 等于 0 表示一直阻塞,time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,也就是相对当前时间累加 time 后当前线程就会被唤醒。

    如果 isAbsolute 等于 true,并且 time 大于 0 表示阻塞后到指定的时间点后会被唤醒,这里 time 是个绝对的时间,是某一个时间点换算为 ms 后的值。

    另外当其它线程调用了当前阻塞线程的 interrupt 方法中断了当前线程时候,当前线程也会返回,当其它线程调用了 unpark 方法并且把当前线程作为参数时候当前线程也会返回。

  • void unpark(Object thread)
    唤醒调用 park 后阻塞的线程,参数为需要唤醒的线程。

下面是 Jdk8 新增的方法,这里简单的列出 Long 类型操作的方法

  • long getAndSetLong(Object obj, long offset, long update) 方法
    获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量 volatile 语义的值为 update。

public final long getAndSetLong(Object obj, long offset, long update)
  {    long l;
    do
    {
      l = getLongVolatile(obj, offset);//(1)
    } while (!compareAndSwapLong(obj, offset, l, update));    return l;
  }

从代码可知内部代码 (1) 处使用 getLongVolatile 获取当前变量的值,然后使用 CAS 原子操作进行设置新值,这里使用 while 循环是考虑到多个线程同时调用的情况 CAS 失败后需要自旋重试。

  • long getAndAddLong(Object obj, long offset, long addValue) 方法
    获取对象 obj 中偏移量为 offset 的变量 volatile 语义的值,并设置变量值为原始值 +addValue。

public final long getAndAddLong(Object obj, long offset, long addValue)
  {    long l;
    do
    {
      l = getLongVolatile(obj, offset);
    } while (!compareAndSwapLong(obj, offset, l, l + addValue));    return l;
  }

类似 getAndSetLong 的实现,只是这里使用CAS的时候使用了原始值+传递的增量参数 addValue 的值。

2.2 如何使用 Unsafe 类

看到 Unsafe 这个类如此牛叉,你肯定会忍不住撸下下面代码,期望能够使用 Unsafe 做点事情。

public class TestUnSafe {    //获取Unsafe的实例(2.2.1)
    static final Unsafe unsafe = Unsafe.getUnsafe();    //记录变量state在类TestUnSafe中的偏移值(2.2.2)
    static final long stateOffset;    //变量(2.2.3)
    private volatile long state=0;    static {        try {            //获取state变量在类TestUnSafe中的偏移值(2.2.4)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

        } catch (Exception ex) {

            System.out.println(ex.getLocalizedMessage());            throw new Error(ex);
        }

    }    public static void main(String[] args) {        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe test = new TestUnSafe();        //(2.2.6)
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

如上代码(2.2.1)获取了 Unsafe 的一个实例,代码(2.2.3)创建了一个变量 state 初始化为 0。

代码(2.2.4)使用 unsafe.objectFieldOffset 获取 TestUnSafe 类里面的 state 变量在 TestUnSafe 对象里面的内存偏移量地址并保存到 stateOffset 变量。

代码(2.2.6)调用创建的 unsafe 实例的 compareAndSwapInt 方法,设置 test 对象的 state 变量的值,具体意思是如果 test 对象内存偏移量为 stateOffset 的 state 的变量为 0,则更新该值为 1。

运行上面代码我们期望会输出 true,然而执行后会输出如下结果:


为研究其原因,必然要翻看 getUnsafe 代码,看看里面做了啥:

private static final Unsafe theUnsafe = new Unsafe(); public static Unsafe getUnsafe(){    //(2.2.7)
    Class localClass = Reflection.getCallerClass();   //(2.2.8)
    if (!VM.isSystemDomainLoader(localClass.getClassLoader())) {      throw new SecurityException("Unsafe");
    }    return theUnsafe;
}  //判断paramClassLoader是不是BootStrap类加载器(2.2.9)
 public static boolean isSystemDomainLoader(ClassLoader paramClassLoader)
  {    return paramClassLoader == null;
  }

代码(2.2.7)获取调用 getUnsafe 这个方法的对象的 Class 对象,这里是 TestUnSafe.class。

代码(2.2.8)判断是不是 Bootstrap 类加载器加载的 localClass,这里是看是不是 Bootstrap 加载器加载了 TestUnSafe.class。很明显由于 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。

思考下,这里为何要有这个判断那?

我们知道 Unsafe 类是在 rt.jar 里面提供的,而 rt.jar 里面的类是使用 Bootstrap 类加载器加载的,而我们启动 main 函数所在的类是使用 AppClassLoader 加载的。

所以在 main 函数里面加载 Unsafe 类时候鉴于委托机制会委托给 Bootstrap 去加载 Unsafe 类。

如果没有代码(2.2.8)这鉴权,那么我们应用程序就可以随意使用 Unsafe 做事情了,而 Unsafe 类可以直接操作内存,是不安全的。

所以 JDK 开发组特意做了这个限制,不让开发人员在正规渠道下使用 Unsafe 类,而是在 rt.jar 里面的核心类里面使用 Unsafe 功能。

那么如果开发人员真的想要实例化 Unsafe 类,使用 Unsafe 的功能该如何做那?

方法有很多种,既然正规渠道访问不了,那么就玩点黑科技,使用万能的反射来获取 Unsafe 实例方法:

public class TestUnSafe {    static final Unsafe unsafe;    static final long stateOffset;    private volatile long state = 0;    static {        try {            // 反射获取 Unsafe 的成员变量 theUnsafe(2.2.10)
            Field field = Unsafe.class.getDeclaredField("theUnsafe");            // 设置为可存取(2.2.11)
            field.setAccessible(true);            // 获取该变量的值(2.2.12)
            unsafe = (Unsafe) field.get(null);            //获取 state 在 TestUnSafe 中的偏移量 (2.2.13)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));

        } catch (Exception ex) {

            System.out.println(ex.getLocalizedMessage());            throw new Error(ex);
        }

    }    public static void main(String[] args) {

        TestUnSafe test = new TestUnSafe();
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);

    }
}

如上代码通过代码(2.2.10),(2.2.11),(2.2.12)反射获取 unsafe 的实例,然后运行结果输出:

三、LockSupport类探究

JDK 中的 rt.jar 里面的 LockSupport 是个工具类,主要作用是挂起和唤醒线程,它是创建锁和其它同步类的基础。

LockSupport 类与每个使用它的线程都会关联一个许可证,默认调用 LockSupport 类的方法的线程是不持有许可证的,LockSupport 内部使用 Unsafe 类实现,下面介绍下 LockSupport 内的几个主要函数:

  • void park() 方法
    如果调用 park() 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,否者调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

如下代码,直接在 main 函数里面调用 park 方法,最终结果只会输出begin park!,然后当前线程会被挂起,这是因为默认下调用线程是不持有许可证的。


    public static void main( String[] args )
    {
        System.out.println( "begin park!" );

        LockSupport.park();

        System.out.println( "end park!" );

    }

在其它线程调用 unpark(Thread thread) 方法并且当前线程作为参数时候,调用park方法被阻塞的线程会返回。

另外其它线程调用了阻塞线程的 interrupt() 方法,设置了中断标志时候或者由于线程的虚假唤醒原因后阻塞线程也会返回,所以调用 park() 最好也是用循环条件判断方式。

需要注意的是调用 park() 方法被阻塞的线程被其他线程中断后阻塞线程返回时候并不会抛出 InterruptedException 异常。

  • void unpark(Thread thread) 方法

    当一个线程调用了 unpark 时候,如果参数 thread 线程没有持有 thread 与 LockSupport 类关联的许可证,则让 thread 线程持有。

    如果 thread 之前调用了 park() 被挂起,则调用 unpark 后,该线程会被唤醒。

    如果 thread 之前没有调用 park,则调用 unPark 方法后,在调用 park() 方法,会立刻返回,上面代码修改如下:

 public static void main( String[] args )
    {
        System.out.println( "begin park!" );        //使当前线程获取到许可证
        LockSupport.unpark(Thread.currentThread());        //再次调用park
        LockSupport.park();

        System.out.println( "end park!" );

    }

则会输出:
begin park!
end park!

下面再来看一个例子来加深对 park,unpark 的理解

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {            @Override
            public void run() {

                System.out.println("child thread begin park!");                // 调用park方法,挂起自己
                LockSupport.park();

                System.out.println("child thread unpark!");

            }
        });        //启动子线程
        thread.start();        //主线程休眠1S
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");        //调用unpark让thread线程持有许可证,然后park方法会返回
        LockSupport.unpark(thread);

    }

输出为:

child thread begin park!
main thread begin unpark!
child thread unpark!

上面代码首先创建了一个子线程 thread,启动后子线程调用 park 方法,由于默认子线程没有持有许可证,会把自己挂起。

主线程休眠 1s 为的是主线程在调用 unpark 方法前让子线程输出 child thread begin park! 并阻塞。

主线程然后执行 unpark 方法,参数为子线程,目的是让子线程持有许可证,然后子线程调用的 park 方法就返回了。

park 方法返回时候不会告诉你是因为何种原因返回,所以调用者需要根据之前是处于什么目前调用的 park 方法,再次检查条件是否满足,如果不满足的话还需要再次调用 park 方法。

例如,线程在返回时的中断状态,根据调用前后中断状态对比就可以判断是不是因为被中断才返回的。

为了说明调用 park 方法后的线程被中断后会返回,修改上面例子代码,删除 LockSupport.unpark(thread); 然后添加 thread.interrupt(); 代码如下:

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {            @Override
            public void run() {

                System.out.println("child thread begin park!");                // 调用park方法,挂起自己,只有被中断才会退出循环
                while (!Thread.currentThread().isInterrupted()) {
                    LockSupport.park();

                }

                System.out.println("child thread unpark!");

            }
        });        // 启动子线程
        thread.start();        // 主线程休眠1S
        Thread.sleep(1000);

        System.out.println("main thread begin unpark!");        // 中断子线程线程
        thread.interrupt();

    }

输出为:

child thread begin park!
main thread begin unpark!
child thread unpark!

如上代码也就是只有当子线程被中断后子线程才会运行结束,如果子线程不被中断,即使你调用 unPark(thread) 子线程也不会结束。

  • void parkNanos(long nanos)函数

和 park 类似,如果调用 park 的线程已经拿到了与 LockSupport 关联的许可证,则调用 LockSupport.park() 会马上返回,不同在于如果没有拿到许可调用线程会被挂起 nanos 时间后在返回。

park 还支持三个带有 blocker 参数的方法,当线程因为没有持有许可的情况下调用 park 被阻塞挂起时候,这个 blocker 对象会被记录到该线程内部。

使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调 getBlocker(Thread) 方法来获取该 blocker 对象的,所以 JDK 推荐我们使用带有 blocker 参数的 park 方法,并且 blocker 设置为 this,这样当内存 dump 排查问题时候就能知道是那个类被阻塞了。

例如下面代码:

public class TestPark {    public  void testPark(){
       LockSupport.park();//(1)

    }    public static void main(String[] args) {

        TestPark testPark = new TestPark();
        testPark.testPark();

    }

}

运行后使用 jstack pid 查看线程堆栈时候可以看到如下:

修改 代码(1)为 LockSupport.park(this) 后运行在 jstack pid 结果为:


可知使用带 blocker 的 park 方法后,线程堆栈可以提供更多有关阻塞对象的信息。

  • park(Object blocker) 函数

public static void park(Object blocker) {   //获取调用线程
    Thread t = Thread.currentThread();   //设置该线程的 blocker 变量
    setBlocker(t, blocker);    //挂起线程
    UNSAFE.park(false, 0L);   //线程被激活后清除 blocker 变量,因为一般都是线程阻塞时候才分析原因
    setBlocker(t, null);
}

Thread 类里面有个变量 volatile Object parkBlocker 用来存放 park 传递的 blocker 对象,也就是把 blocker 变量存放到了调用 park 方法的线程的成员变量里面。

  • void parkNanos(Object blocker, long nanos) 函数
    相比 park(Object blocker) 多了个超时时间。

  • void parkUntil(Object blocker, long deadline)
    parkUntil 的代码如下:

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);       //isAbsolute=true,time=deadline;表示到 deadline 时间时候后返回
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

可知是设置一个 deadline,时间单位为 milliseconds,是从 1970 到现在某一个时间点换算为毫秒后的值,这个和 parkNanos(Object blocker, long nanos) 区别是后者是从当前算等待 nanos 时间,而前者是指定一个时间点。

比如我需要等待到 2017.12.11 日 12:00:00,则吧这个时间点转换为从 1970 年到这个时间点的总毫秒数。

最后在看一个例子

class FIFOMutex {    private final AtomicBoolean locked = new AtomicBoolean(false);    private final Queue waiters = new ConcurrentLinkedQueue();    public void lock() {        boolean wasInterrupted = false;
        Thread current = Thread.currentThread();
        waiters.add(current);        // 只有队首的线程可以获取锁(1)
        while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
            LockSupport.park(this);            if (Thread.interrupted()) // (2)
                wasInterrupted = true;
        }

        waiters.remove();        if (wasInterrupted) // (3)
            current.interrupt();
    }    public void unlock() {
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}

这是一个先进先出的锁,也就是只有队列首元素可以获取锁,代码(1)处如果当前线程不是队首或者当前锁已经被其它线程获取,则调用park方法挂起自己。

然后代码(2)处判断,如果 park 方法是因为被中断而返回,则忽略中断,并且重置中断标志,只做个标记,然后再次判断当前线程是不是队首元素或者当前锁是否已经被其它线程获取,如果是则继续调用 park 方法挂起自己。

然后代码(3)中如果标记为 true 则中断该线程,这个怎么理解那?其实意思是其它线程中断了该线程,虽然我对中断信号不感兴趣,忽略它,但是不代表其它线程对该标志不感兴趣,所以要恢复下。

四、 LongAdder 和 LongAccumulator 原理探究

  • 抽象同步队列 AQS (AbstractQueuedSynchronizer) 概述,AQS 是实现同步的基础组件,并发包中锁的实现底层就是使用 AQS 实现,虽然大多数开发者可能从来不会直接用到 AQS,但是知道其原理对于架构设计还是很有帮助的。

  • 独占锁 ReentrantLock 原理探究,ReentrantLock 是可重入的独占锁或者叫做排它锁,同时只能有一个线程可以获取该锁,其实现分为公平与非公平的独占锁。

  • 读写锁 ReentrantReadWriteLock 原理,ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求,所以 ReentrantReadWriteLock 应运而生,本文来介绍读写分离锁的实现。

  • StampedLock 锁原理探究,StampedLock 是并发包里面 jdk8 版本新增的一个锁,该锁提供了三种模式的读写控制。

二、抽象同步队列 AQS 概述

2.1 AQS - 锁的底层支持

AbstractQueuedSynchronizer 抽象同步队列, 简称 AQS,是实现同步器的基础组件,并发包中锁的实现底层就是使用 AQS 实现,另外大多数开发者可能从来不会直接用到 AQS,但是知道其原理对于架构设计还是很有帮助的,下面看下 AQS 的类图结构:

Java 编程之美:并发编程_第10张图片

AQS 是一个 FIFO 的双向队列,内部通过节点 head 和 tail 记录队首和队尾元素,队列元素类型为 Node。

其中 Node 中 thread 变量用来存放进入 AQS 队列里面的线程;Node 节点内部 SHARED 用来标记该线程是获取共享资源时候被阻塞挂起后放入 AQS 队列,EXCLUSIVE 标示线程是获取独占资源时候被挂起后放入 AQS 队列;

waitStatus 记录当前线程等待状态,分别为 CANCELLED(线程被取消了),SIGNAL(线程需要被唤醒),CONDITION(线程在条件队列里面等待),PROPAGATE(释放共享资源时候需要通知其它节点);prev 记录当前节点的前驱节点,next 记录当前节点后继节点。

AQS 中维持了一个单一的状态信息 state, 可以通过 getState,setState,compareAndSetState 函数修改其值;对于 ReentrantLock 的实现来说,state 可以用来表示当前线程获取锁的可重入次数;

对应读写锁 ReentrantReadWriteLock 来说 state 的高 16 位表示读状态也就是获取该读锁的次数,低 16 位表示获取到写锁的线程的可重入次数;对于 semaphore 来说 state 用来表示当前可用信号的个数;

对于 FutuerTask 来说,state 用来表示任务状态(例如还没开始,运行,完成,取消);对应 CountDownlatch 和 CyclicBarrie 来说 state 用来表示计数器当前的值。

AQS 有个内部类 ConditionObject 是用来结合锁实现线程同步,ConditionObject 可以直接访问 AQS 对象内部的变量,比如 state 状态值和 AQS 队列;

ConditionObject 是条件变量,每个条件变量对应着一个条件队列 (单向链表队列),用来存放调用条件变量的 await() 方法后被阻塞的线程,如类图,这个条件队列的头尾元素分别为 firstWaiter 和 lastWaiter。

对于 AQS 来说线程同步的关键是对状态值 state 进行操作,根据 state 是否属于一个线程,操作 state 的方式分为独占模式和共享模式。
独占方式下获取和释放资源使用方法为:

void acquire(int arg)
void acquireInterruptibly(int arg)
boolean release(int arg)

共享模式下获取和释放资源方法为:

void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)

对于独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是那个线程获取到了,其它线程尝试操作 state 获取资源时候发现当前该资源不是自己持有的,就会获取失败后被阻塞;

比如独占锁 ReentrantLock 的实现,当一个线程获取了 ReentrantLock 的锁后,AQS 内部会首先使用 CAS 操作把 state 状态值从 0 变为 1,然后设置当前锁的持有者为当前线程。

当该线程再次获取锁时候发现当前线程就是锁的持有者则会把状态值从 1 变为 2 也就是设置可重入次数,当另外一个线程获取锁的时候发现自己并不是该锁的持有者就会被放入 AQS 阻塞队列后挂起。

对应共享操作方式资源是与具体线程不相关的,多个线程去请求资源时候是通过 CAS 方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取时候如果当前资源还能满足它的需要,则当前线程只需要使用 CAS 方式进行获取即可,共享模式下并不需要记录那个线程获取了资源;

比如 Semaphore 信号量,当一个线程通过 acquire() 方法获取一个信号量时候,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋 CAS 获取信号量。

对应独占模式的获取与释放资源流程:

1)当一个线程调用 acquire(int arg) 方法获取独占资源时候,会首先使用 tryAcquire 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回。失败则将当前线程封装为类型为 Node.EXCLUSIVE 的 Node 节点后插入到 AQS 阻塞队列尾部,并调用 LockSupport.park(this) 挂起当前线程。

public final void acquire(int arg) {        
  if (!tryAcquire(arg) &&    
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))      
      selfInterrupt();    
}

2)当一个线程调用 release(int arg) 时候会尝试使用, tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用 LockSupport.unpark(thread) 激活 AQS 队列里面最早被阻塞的线程 (thread)。被激活的线程则使用 tryAcquire 尝试看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活然后继续向下运行,否者还是会被放入 AQS 队列并被挂起。

public final boolean release(int arg) {      
    if (tryRelease(arg)) {            
        Node h = head;            
        if (h != null && h.waitStatus != 0)                
            unparkSuccessor(h);            
         return true;        
         }        
       return false;    
   }

需要注意的 AQS 类并没有提供可用的 tryAcquire 和 tryRelease,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquire 和 tryRelease 需要有具体的子类来实现。

子类在实现 tryAcquire 和 tryRelease 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true, 否者返回 false。

子类还需要定义在调用 acquire 和 release 方法时候 state 状态值的增减代表什么含义。

比如继承自 AQS 实现的独占锁 ReentrantLock,定义当 status 为 0 的时候标示锁空闲,为 1 的时候标示锁已经被占用,在重写 tryAcquire 时候,内部需要使用 CAS 算法看当前 status 是否为 0,如果为 0 则使用 CAS 设置为 1,并设置当前线程的持有者为当前线程,并返回 true, 如果 CAS 失败则 返回 false。

比如继承自 AQS 实现的独占锁实现 tryRelease 时候,内部需要使用 CAS 算法把当前 status 值从 1 修改为 0,并设置当前锁的持有者为 null,然后返回 true, 如果 cas 失败则返回 false。

对应共享资模式的获取与释放流程:

1)当线程调用 acquireShared(int arg) 获取共享资源时候,会首先使用 tryAcquireShared 尝试获取资源,具体是设置状态变量 state 的值,成功则直接返回。失败则将当前线程封装为类型为 Node.SHARED 的 Node 节点后插入到 AQS 阻塞队列尾部,并使用 LockSupport.park(this) 挂起当前线程。

    public final void acquireShared(int arg) {        
       if (tryAcquireShared(arg) < 0)            
           doAcquireShared(arg);    
   }

2)当一个线程调用 releaseShared(int arg) 时候会尝试使用, tryReleaseShared 操作释放资源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队列里面最早被阻塞的线程 (thread)。

被激活的线程则使用 tryReleaseShared 尝试看当前状态变量 state 的值是否能满足自己的需要,满足则该线程被激活然后继续向下运行,否者还是会被放入 AQS 队列并被挂起。

 public final boolean releaseShared(int arg) {        
        if (tryReleaseShared(arg)) {            
            doReleaseShared();            
            return true;        
        }        
      return false;    
  }

同理需要注意的 AQS 类并没有提供可用的 tryAcquireShared 和 tryReleaseShared,正如 AQS 是锁阻塞和同步器的基础框架,tryAcquireShared 和 tryReleaseShared 需要有具体的子类来实现。

子类在实现 tryAcquireShared 和 tryReleaseShared 时候要根据具体场景使用 CAS 算法尝试修改状态值 state, 成功则返回 true, 否者返回 false。

比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryAcquireShared 时候,首先看写锁是否被其它线程持有,如果是则直接返回 false,否者使用 cas 递增 status 的高 16 位,在 ReentrantReadWriteLock 中 status 的高 16 为获取读锁的次数。

比如继承自 AQS 实现的读写锁 ReentrantReadWriteLock 里面的读锁在重写 tryReleaseShared 时候,内部需要使用 CAS 算法把当前 status 值的高 16 位减一,然后返回 true, 如果 cas 失败则返回 false。

基于 AQS 实现的锁除了需要重写上面介绍的方法,还需要重写 isHeldExclusively 方法用来判断锁是被当前线程独占还是被共享。

另外也许你会好奇独占模式下的

void acquire(int arg)
void acquireInterruptibly(int arg)

和共享模式下获取资源的:

void acquireShared(int arg) 
void acquireSharedInterruptibly(int arg)

这两套函数都有一个带有 Interruptibly 关键字的函数,那么带有这个关键字的和不带的有什么区别那?

其实不带 Interruptibly 关键字的方法是说不对中断进行响应,也就是线程在调用不带 Interruptibly 关键字的方法在获取资源的时候或者获取资源失败被挂起时候,其他线程中断了该线程,那么该线程不会因为被中断而抛出异常,还是继续获取资源或者被挂起,也就是不对中断进行响应,忽略中断。

带 Interruptibly 关键字的方法是说对中断进行响应,也就是也就是线程在调用带 Interruptibly 关键字的方法在获取资源的时候或者获取资源失败被挂起时候,其他线程中断了该线程,那么该线程会抛出 InterruptedException 异常而返回。

本节最后我们来看看 AQS 提供的队列是如何维护的,主要看入队操作

  • 入队操作: 当一个线程获取锁失败后该线程会被转换为 Node 节点,然后就会使用 enq(final Node node) 方法插入该节点到 AQS 的阻塞队列,

    private Node enq(final Node node) {        
       for (;;) {            
           Node t = tail;//(1)            
           if (t == null) { // Must initialize                
               if (compareAndSetHead(new Node()))//(2)                    
                   tail = head;            
               } else {                
                   node.prev = t;//(3)                
                   if (compareAndSetTail(t, node)) {//(4)                    
                       t.next = node;                    
                       return t;                
                   }            
               }        
            }    
        }

下面结合代码和下面的节点图来讲解下,如上代码第一次循环当要在 AQS 队列尾部插入元素时候,AQS 队列状态为图(default), 也就是队列头尾节点都指向 null;当执行代码(1)后节点 t 指向了尾部节点,这时候队列状态如图(I)。

可知这时候 t 为 null,则执行代码(2)使用 CAS 算法设置一个哨兵节点为头结点,如果 CAS 设置成功,然后让尾部节点也指向哨兵节点,这时候队列状态如图(II)。

到现在只是插入了一个哨兵节点,还需要插入的 node 节点,所以第二次循环后执行到步骤(1),这时候队列状态如图(III);然后执行代码(3)设置 node 的前驱节点为尾部节点,这时候队列状态图如图(IV);然后通过 CAS 算法设置 node 节点为尾部节点,CAS 成功后队列状态图为(V);CAS 成功后在设置原来的尾部节点的后驱节点为 node, 这时候就完成了双向链表的插入了,这时候队列状态为图(VI)。

2.2 AQS - 条件变量的支持

正如基础篇讲解的 notify 和 wait 是配合 synchronized 内置锁实现线程间同步基础设施,条件变量的 signal 和 await 方法是用来配合锁(使用 AQS 实现的锁)实现线程间同步的基础设施。

在基础篇讲解了在调用共享变量的 notify 和 wait 方法前必须先获取该共享变量的内置锁,同理在调用条件变量的 signal 和 await 方法前必须先获取条件变量对应的锁。

说了那么多,到底什么是条件变量那?如何使用那?不急,下面看一个例子:

ReentrantLock lock = new ReentrantLock();//(1)
Condition condition = lock.newCondition();//(2)

lock.lock();//(3)
try {    
   System.out.println("begin wait");    
   condition.await();//(4)    
   System.out.println("end wait");
} catch (Exception e) {    
   e.printStackTrace();
} finally {    
   lock.unlock();//(5)
}
lock.lock();//(6)
try {    
   System.out.println("begin signal");    
   condition.signal();//(7)    
   System.out.println("end signal");
} catch (Exception e) {    
   e.printStackTrace();
} finally {    
   lock.unlock();//(8)
}

如上代码(1)创建了一个独占锁 ReentrantLock 的对象,ReentrantLock 是基于 AQS 实现的锁。

代码(2)使用创建的 lock 对象的 newCondition()方法创建了一个 ConditionObject 变量,这个变量就是 lock 锁对应的一个条件变量。需要注意的是一个 Lock 对象可以创建多个条件变量。

代码(3)首先获取了独占锁,代码(4) 则调用了条件变量的 await()方法阻塞挂起了当前线程,当其它线程调用了条件变量的 signal 方法时候,被阻塞的线程才会从 await 处返回,需要注意的是和调用 Object 的 wait 方法一样,如果在没有获取到锁前调用了条件变量的 await 方法会抛出 java.lang.IllegalMonitorStateException 异常。

代码(5) 则释放了获取的锁。

其实这里的 lock 对象等价于 synchronized 加上共享变量,当调用 lock.lock()方法就相当于进入了 synchronized 块(获取了共享变量的内置锁),当调用 lock.unLock() 方法时候就相当于退出了 synchronized 块。

 当调用条件变量的 await() 方法时候就相当于调用了共享变量的 wait() 方法,当调用了条件变量的 signal 方法时候就相当于调用了共享变量的 notify() 方法。当调用了条件变量的 signalAll()方法时候就相当于调用了共享变量的 notifyAll() 方法。

有了上面的解释相信大家对条件变量是什么,用来做什么用的有了一定的认识了。

上面通过 lock.newCondition() 作用其实是 new 了一个 AQS 内部声明的 ConditionObject 对象,ConditionObject 是 AQS 的内部类,可以访问到 AQS 内部的变量(例如状态变量 status 变量)和方法。

对应每个条件变量内部维护了一个条件队列,用来存放当调用条件变量的 await() 方法被阻塞的线程。注意这个条件队列和 AQS 队列不是一回事情。

如下代码,当线程调用了条件变量的 await() 方法时候(事先必须先调用了锁的 lock() 方法获取锁),内部会构造一个类型为 Node.CONDITION 的 node 节点,然后插入该节点到条件队列末尾,然后当前线程会释放获取的锁(也就是会操作锁对应的 status 变量的值),并被阻塞挂起。

这时候如果有其它线程调用了 lock.lock() 尝试获取锁时候,就会有一个线程获取到锁,如果获取到锁的线程有调用了条件变量的 await()方法,则该线程也会被放入条件变量的阻塞队列,然后释放获取到的锁,阻塞到 await() 方法处。

 public final void await() throws InterruptedException {            
        if (Thread.interrupted())                
            throw new InterruptedException();            
            //创建新的node,并插入到条件队列末尾(9)            
            Node node = addConditionWaiter();            
            //释放当前线程获取的锁(10)            
            int savedState = fullyRelease(node);            
            int interruptMode = 0;            
           //调用park方法阻塞挂起当前线程(11)            
           while (!isOnSyncQueue(node)) {                
               LockSupport.park(this);                
               if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)                    
               break;            
            }            
            ...      
      }

如下代码,当另外一个线程调用了条件变量的 signal 方法时候(事先必须先调用了锁的 lock() 方法获取锁),内部会把条件队列里面队头的一个线程节点从条件队列里面移除后放入到 AQS 的阻塞队列里面,然后激活这个线程。

 public final void signal() {       
    if (!isHeldExclusively())          
        throw new IllegalMonitorStateException();      
      Node first = firstWaiter;      
      if (first != null)          
          //移动条件队列队头元素到AQS队列          
          doSignal(first);
  }

需要注意的是 AQS 只提供了 ConditionObject 的实现,并没有提供 newCondition 函数来 new 一个 ConditionObject 对象,需要由 AQS 的子类来提供 newCondition 函数。

下面来看下当一个线程调用条件变量的 await() 方法被阻塞后,如何放入的条件队列。

private Node addConditionWaiter() {            
   Node t = lastWaiter;            
   ...            
   //(1)            
   Node node = new Node(Thread.currentThread(), Node.CONDITION);            
   //(2)            
   if (t == null)                
       firstWaiter = node;            
   else                
       t.nextWaiter = node;//(3)            
       lastWaiter = node;//(4)            
       return node;    
}

如上代码(1)首先根据当前线程创建一个类型为 Node.CONDITION 的节点,然后通过步骤(2)(3)(4)在单向条件队列尾部插入一个元素。

注:当多个线程同时调用 lock.lock() 获取锁的时候,同时只有一个线程获取到了该锁,其他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里面,并做自旋 CAS 尝试获取锁;

如果获取到锁的线程又调用了对应的条件变量的 await() 方法,则该线程会释放获取到的锁,并被转换为 Node 节点插入到条件变量对应的条件队列里面;

这时候因为调用 lock.lock() 方法被阻塞到 AQS 队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的 await()方法则该线程也会被放入条件变量的条件队列。

当另外一个线程调用了条件变量的 signal() 或者 signalAll() 方法时候,会把条件队列里面的一个或者全部 Node 节点移动到 AQS 的阻塞队列里面,等待时机获取锁。

Java 编程之美:并发编程_第11张图片

最后一个图总结下,一个锁对应有一个 AQS 阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

三、独占锁 ReentrantLock 原理

3.1 类图结构简介

ReentrantLock 是可重入的独占锁,同时只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞后放入该锁的 AQS 阻塞队列里面。首先一览 ReentrantLock 的类图以便对它的实现有个大致了解

从类图可知 ReentrantLock 最终还是使用 AQS 来实现,并且根据参数决定内部是公平还是非公平锁,默认是非公平锁:

 public ReentrantLock() {        
        sync = new NonfairSync();  
   }    
public ReentrantLock(boolean fair) {    
   sync = fair ? new FairSync() : new NonfairSync();
   }

其中类 Sync 直接继承自 AQS,它的子类 NonfairSync 和 FairSync 分别实现了获取锁的公平和非公平策略。

在这里 AQS 的状态值 state 代表线程获取该锁的可重入次数,默认情况下 state 的值为 0 标示当前锁没有被任何线程持有,当一个线程第一次获取该锁时候会使用尝试使用 CAS 设置 state 的值为 1。

如果 CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程,在该线程没有释放锁第二次获取改锁后状态值被为 2,这就是可重入次数,在该线程释放该锁的时候,会尝试使用 CAS 让状态值减一,如果减一后状态值为 0 则当前线程释放该锁。

3.2 获取锁

  • void lock()
    当一个线程调用该方法,说明该线程希望获取该锁,如果锁当前没有被其它线程占用并且当前线程之前没有获取该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 后直接返回。

    如果当前线程之前已经获取过该锁,则这次只是简单的把 AQS 的状态值 status 加 1 后返回。

    如果该锁已经被其它线程持有,则调用该方法的线程会被放入 AQS 队列后阻塞挂起。

public void lock() {        
       sync.lock();  
    }

如上代码 ReentrantLock 的 lock() 是委托给了 sync 类,根据创建 ReentrantLock 时候构造函数选择 sync 的实现是 NonfairSync 或者 FairSync,这里先看 sync 的子类 NonfairSync 的情况,也就是非公平锁的时候:

final void lock() {  
 //(1)CAS设置状态值  
 if (compareAndSetState(0, 1))      
     setExclusiveOwnerThread(Thread.currentThread());  
 else  
 //(2)调用AQS的acquire方法      
     acquire(1);
}

如上代码(1)因为默认 AQS 的状态值为 0,所以第一个调用 Lock 的线程会通过 CAS 设置状态值为 1,CAS 成功则表示当前线程获取到了锁,然后 setExclusiveOwnerThread 设置了该锁持有者是当前线程。

如果这时候有其它线程调用 lock 方法企图获取该锁执行代码(1)CAS 会失败,然后会调用 AQS 的 acquire 方法,这里注意传递参数为 1,这里在贴下 AQS 的 acquire 骨干代码:

public final void acquire(int arg) {        
   //(3)调用ReentrantLock重写的tryAcquire方法        
   if (!tryAcquire(arg) &&            
       // tryAcquiref返回false会把当前线程放入AQS阻塞队列            
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            
       selfInterrupt();  
 }

之前说过 AQS 并没有提供可用的 tryAcquire 方法,tryAcquire 方法需要子类自己定制化,所以这里代码(3)会调用 ReentrantLock 重写的 tryAcquire 方法代码,这里先看下非公平锁的代码如下:

 protected final boolean tryAcquire(int acquires) {            
            return nonfairTryAcquire(acquires);
 }
final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    //(4)当前AQS状态值为0    
    if (c == 0) {      
        if (compareAndSetState(0, acquires)) {            
            setExclusiveOwnerThread(current);          
            return true;    
         }  
     }//(5)当前线程是该锁持有者  
     else if (current == getExclusiveOwnerThread()) {      
          int nextc = c + acquires;      
          if (nextc < 0) // overflow          
              throw new Error("Maximum lock count exceeded");      setState(nextc);      
              return true;  
      }//(6)  
      return false;
  }

如上代码(4)会看当前锁的状态值是否为 0,为 0 则说明当前该锁空闲,那么就尝试 CAS 获取该锁(尝试将 AQS 的状态值从 0 设置为 1),并设置当前锁的持有者为当前线程返回返回 true。

如果当前状态值不为 0 则说明该锁已经被某个线程持有,所以代码(5)看当前线程是否是该锁的持有者,如果当前线程是该锁持有者,状态值增加 1 然后返回 true。

如果当前线程不是锁的持有者则返回 false, 然后会被放入 AQS 阻塞队列。

这里介绍完了非公平锁的实现代码,回过头来看看非公平在这里是怎么体现的,首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。

这里假设线程 A 调用 lock()方法时候执行到了 nonfairTryAcquire 的代码(4)发现当前状态值不为 0,所以执行代码(5)发现当前线程不是线程持有者,则执行代码(6)返回 false,然后当前线程会被放入了 AQS 阻塞队列。

这时候线程 B 也调用了 lock() 方法执行到 nonfairTryAcquire 的代码(4)时候发现当前状态值为 0 了(假设占有该锁的其它线程释放了该锁)所以通过 CAS 设置获取到了该锁。

而明明是线程 A 先请求获取的该锁那,这就是非公平锁的实现,这里线程 B 在获取锁前并没有看当前 AQS 队列里面是否有比自己请求该锁更早的线程,而是使用了抢夺策略。

那么下面看看公平锁是怎么实现公平的,公平锁的话只需要看 FairSync 重写的 tryAcquire 方法

protected final boolean tryAcquire(int acquires) {            
   final Thread current = Thread.currentThread();            
   int c = getState();            
   //(7)当前AQS状态值为0            
   if (c == 0) {            
   //(8)公平性策略                
       if (!hasQueuedPredecessors() &&                    
           compareAndSetState(0, acquires)) {                    
           setExclusiveOwnerThread(current);                    
           return true;                
        }          
    }            
    //(9)当前线程是该锁持有者            
    else if (current == getExclusiveOwnerThread()) {                
         int nextc = c + acquires;                
         if (nextc < 0)                    
             throw new Error("Maximum lock count exceeded");                
             setState(nextc);                
             return true;            
      }//(10)            
     return false;  
   }  
}

如上代码公平性的 tryAcquire 策略与非公平的类似,不同在于代码(8)处在设置 CAS 前添加了 hasQueuedPredecessors 方法,该方法是实现公平性的核心代码,代码如下:

public final boolean hasQueuedPredecessors() {        
   Node t = tail; // Read fields in reverse initialization order        
   Node h = head;        
   Node s;        
   return h != t &&            
       ((s = h.next) == null || s.thread != Thread.currentThread());  
 }

如上代码如果当前线程节点有前驱节点则返回 true,否者如果当前 AQS 队列为空或者当前线程节点是 AQS 的第一个节点则返回 false.

其中如果 h==t 则说明当前队列为空则直接返回 false,如果 h!=t 并且 s==null 说明有一个元素将要作为 AQS 的第一个节点入队列,那么返回 true, 如果 h!=t 并且 s!=null 并且 s.thread != Thread.currentThread() 则说明队列里面的第一个元素不是当前线程则返回 true.

  • void lockInterruptibly()
    与 lock() 方法类似,不同在于该方法对中断响应,就是当前线程在调用该方式时候,如果其它线程调用了当前线程线程的 interrupt()方法,当前线程会抛出 InterruptedException 异常然后返回

public void lockInterruptibly() throws InterruptedException {        
       sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)       throws InterruptedException {  
       //当前线程被中断则直接抛出异常  
       if (Thread.interrupted())      
           throw new InterruptedException();  
  //尝试获取资源  
  if (!tryAcquire(arg))      
      //调用AQS可被状态的方法      
      doAcquireInterruptibly(arg);
}
  • boolean tryLock()
    尝试获取锁,如果当前该锁没有被其它线程持有则当前线程获取该锁并返回 true, 否者返回 false,注意该方法不会引起当前线程阻塞。

public boolean tryLock() {   
   return sync.nonfairTryAcquire(1);
}

final
boolean nonfairTryAcquire(int acquires)
{  
   final Thread current = Thread.currentThread();  
   int c = getState();  
   if (c == 0) {      
       if (compareAndSetState(0, acquires)) {              setExclusiveOwnerThread(current);          
             return true;  
        }
    }  
    else if (current == getExclusiveOwnerThread()) {      
         int nextc = c + acquires;      
         if (nextc < 0) // overflow              
             throw new Error("Maximum lock count exceeded");          
         setState(nextc);      
         return true;  
    }  
    return false;
 }

如上代码与非公平锁的 tryAcquire() 方法类似,所以 tryLock() 使用的是非公平策略。

  • boolean tryLock(long timeout, TimeUnit unit)
    尝试获取锁与 tryLock()不同在于设置了超时时间,如果超时没有获取该锁则返回 false。

public boolean tryLock(long timeout, TimeUnit unit)            
      throws InterruptedException
{            
      //调用AQS的tryAcquireNanos方法。        
      return sync.tryAcquireNanos(1, unit.toNanos(timeout));  
}

3.3 释放锁

  • void unlock()
    尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS 状态值减一,如果减去 1 后当前状态值为 0 则当前线程会释放对该锁的持有,否者仅仅减一而已。如果当前线程没有持有该锁调用了该方法则会抛出 IllegalMonitorStateException 异常 ,代码如下:

public void unlock() {        
  sync.release(1);  
}  
protected final boolean tryRelease(int releases) {      
   //(11)如果不是锁持有者调用UNlock则抛出异常。      
  int c = getState() - releases;      
  if (Thread.currentThread() != getExclusiveOwnerThread())          
      throw new IllegalMonitorStateException();      
  boolean free = false;      
  //(12)如果当前可重入次数为0,则清空锁持有线程      
  if (c == 0) {          
               free = true;          
               setExclusiveOwnerThread(null);      
  }      
  //(13)设置可重入次数为原始值-1      
  setState(c);      
  return free;  
}

如上代码(11)如果当前线程不是该锁持有者则直接抛出异常,否者看状态值剩余值是否为 0,为 0 则说明当前线程要释放对该锁的持有权,则执行(12)把当前锁持有者设置为 null。如果剩余值不为 0,则仅仅让当前线程对该锁的可重入次数减一。

3.4 案例介绍

下面使用 ReentrantLock 来实现一个简单的线程安全的 list:

public static class ReentrantLockList {        
       //线程不安全的list        
       private ArrayList array = new ArrayList();        
       //独占锁        
       private volatile ReentrantLock lock = new ReentrantLock();        
       //添加元素        
       public void add(String e) {            
           lock.lock();            
           try {                
               array.add(e);            
            } finally {                
                   lock.unlock();        
            }
       }        
       //删元素        
       public void remove(String e) {            
               lock.lock();            
           try {                
               array.remove(e);            
           } finally {                
               lock.unlock();  
           }    
       }        
       //获取数据        
       public String get(int index) {            
           lock.lock();            
           try {        
                return array.get(index);          
           } finally {                
                lock.unlock();        
           }        
       }

如上代码通过在操作 array 元素前进行加锁保证同时只有一个线程可以对 array 数组进行修改,但是同时也只能有一个线程对 array 元素进行访问。

同理最后使用几个图来加深理解:

Java 编程之美:并发编程_第12张图片

如上图,假如线程 Thread1,Thread2,Thread3 同时尝试获取独占锁 ReentrantLock,假设 Thread1 获取到了,则 Thread2 和 Thread3 就会被转换为 Node 节点后放入 ReentrantLock 对应的 AQS 阻塞队列后阻塞挂起。

Java 编程之美:并发编程_第13张图片

如上图,假设 Thread1 获取锁后调用了对应的锁创建的条件变量 1,那么 Thread1 就会释放获取到的锁,然后当前线程就会被转换为 Node 节点后插入到条件变量 1 的条件队列,由于 Thread1 释放了锁。

所以阻塞到 AQS 队列里面 Thread2 和 Thread3 就有机会获取到该锁,假如使用的公平策略,那么这时候 Thread2 会获取到该锁,会从 AQS 队列里面移除 Thread2 对应的 Node 节点。

四、读写锁 ReentrantReadWriteLock 原理

在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求。

所以 ReentrantReadWriteLock 应运而生,ReentrantReadWriteLock 采用读写分离,多个线程可以同时获取读锁。



你可能感兴趣的:(Java 编程之美:并发编程)