concurrent包的实现
由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
- A线程写volatile变量,随后B线程读这个volatile变量。
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从 本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改 -写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包 得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使 用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
CAS
CAS:Compare and Swap, 翻译成比较并交换。
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
本文先从CAS的应用说起,再深入原理解析。
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
非阻塞算法 (nonblocking algorithms)
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。
这样才获取变量的值的时候才能直接读取。
public final int get() { return value; }
然后来看看++i是怎么做到的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
在这里采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。
而compareAndSet利用JNI来完成CPU指令的操作:
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
其中
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
类似:
if (this == expect) { this = update return true; } else { return false; }
那么问题就来了,成功过程中需要2个步骤:比较this == expect,替换this = update,compareAndSwapInt如何这两个步骤的原子性呢? 参考CAS的原理。
CAS原理
CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。
而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
可以看到这是个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)。下面是对应于intel x86处理器的源代码的片段:
// Adding a lock prefix to an instruction on MP machine // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange int mp = os::is_MP(); __asm { mov edx, dest mov ecx, exchange_value mov eax, compare_value LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } }
如上面源代码所示,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加 上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏 障效果)。
intel的手册对lock前缀的说明如下:
- 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他 处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全 包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行 的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
再看一下最后native方法的源码 jdk8u: atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value;
汇编指令 我们看这一条
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
__asm__表示汇编指令,lock表示锁,if如果 mp(%4)表示cpu是多核, cmpxchgl表示 cmp exchange 全称 compare and exchange。最终实现:
lock cmpxchg 指令
这条汇编指令(硬件指令)表示如果是多核CPU则加上锁。
备注知识:
关于CPU的3种锁及cas的优缺点见《原子操作的实现原理(锁和循环CAS)》
Unsafe
unsafe的详情见《Unsafe》
AQS:
AbstractQueuedSynchronizer:叫做抽象队列同步器,它是一个实现了同步器功能的基础框架。
如下图所示,就是一个普通的并发工具同步器应该具有的功能。
(1)首先线程1去同步器获取资源,即获取锁,获取锁成功直接执行业务方法
(2)线程2也获取锁,但是获取锁失败,就会进入等待队列,阻塞等待
(3)当线程1释放锁的时候,需要去唤醒还在等待锁的线程2,然后线程2苏醒之后继续去尝试获取锁。
上面的红色部分,包括资源、获取失败进行等待队列、释放资源唤醒等待队列线程、线程苏醒重新竞争锁,AQS就是封装了这些通用的流程和功能。而获取锁、释放锁是非通用的,留给具体的同步器去实现。比如:ReentrantLock、CountDownLatch、Semaphore等使用了AQS框架的基本机制,然后自己实现了获取锁、释放锁的具体逻辑,这样就形成了不同的同步器了。
2.2、AQS内部有那些东西?
从整体上给你说说,AQS内部封装了哪些数据结构和功能,后面再介绍一下这些东西:
(1)首先对资源进行了定义,使用一个volatile int state表示资源
(2)规定了获取独占资源的入口为acquire()方法、释放独占资源的入口release()
(3)规定了获取共享资源的入口acquireShared()、释放独占资源入口 releaseShared()方法
(4)声明了实际获取资源、释放资源的具体实现方法,AQS这里只是声明,由子类去重写,实现获取和释放的逻辑。
实际获取独占资源、实际释放独占资源、实际获取共享资源、实际释放共享资源的入口方法,没有具体的实现,让子类实现这些方法从而形成不同的同步工具,这些抽象方法包括:
(4-1)实际获取独占资源的方法,具体实现逻辑封装在子类的tryAcquire()方法内部
(4-2)实际释放独占资源的方法,具体实现逻辑封装在子类的tryRelease()方法内部
(4-3)实际获取共享资源的方法,具体实现逻辑封装在子类的tryAcquireShared()方法内部
(4-4)实际释放共享资源的方法,具体实现逻辑封装在子类的tryReleaseShared()方法内部
(5)封装了一个Node的数据结构,用来存储线程信息。通过多个Node串成一个双向链表的等待队列;链表存储了获取锁失败而进入等待队列的线程
(6)封装了一套非常核心的,线程获取资源失败而如何进入等待队列;以及释放资源之后怎么唤醒等待队列中的线程再次竞争资源的这么一套机制。
这套机制非常核心,基于AQS之上的同步工具类底层都是使用这套机制来实现的。
2.2.1、对资源进行定义 (state 表示资源)
作为一个同步工具,肯定是存在一个多个线程可以共同访问的资源,通过这个资源的状态可以控制各个线程并发时候的行为(也就是通过是否获取锁控制不同线程的行为)。
所以啊,AQS底层肯定也会有资源的概念,AQS使用的是一个volatile int state的变量表示资源。
AQS只是说state表示的是资源,至于上层的子类怎么使用state,state表示什么意思是子类决定的,AQS它自己不管的哈。
(1)比如ReentrantLock中,表示state就表示互斥锁的状态,state = 0表示没人加锁,state > 0 表示有人加锁了;比如Semaphore就使用state表示信号量的个数。
(2)比如state = 10就表示有10个信号量,state > 0的时候信号量还有剩余,别的线程可以去获取,state = 0的时候表示没信号量了,这个时候再去获取就要等待了。
比如下图,AQS使用state表示资源,多线程并发竞争资源:
AQS对资源进行了声明,也就是告诉子类,我内部的state变量表示的是资源(资源代表什么子类自己定)。至于子类你怎么使用我管不着。既然AQS定义了什么是资源,所以AQS同时肯定会定义一套获取资源、释放资源的入口或者规定,如果要基于AQS实现同步工具类啊,就都要遵守AQS的这套约定,我下面就给你说说。
2.2.2、对获取和释放独占锁的入口进行规定
(1)accquire(int arg):获取独占锁入口,获取独占锁需要调用这个方法
(2)acquireInterruptibly(int arg):跟上面的acquire()一样,也是获取独占锁的入口,不过是允许中断的,获取过程中线程被中断了就会抛出异常。
(3)release(int arg):释放独占锁的入口,当释放锁的时候调用这个方法
2.2.3、对获取和释放共享锁的入口进行规定
(1)acquireShare(int arg):获取共享锁入口,获取的时候需要调用这个方法
(2)acquireShareInterruptibly(int arg) :跟上面的acquireShare(int arg)一样,也是获取共享锁的入口,不过是允许中断的,获取过程中线程被中断就会抛出异常。
(3)releaseShare(int arg):释放共享锁的入口,释放共享锁的时候调用这个方法
画个图讲解一下上面的这些入口方法:
获取和是释放独占锁的流程如下图:
获取和释放共享锁的入口如下图:
AQS规定了获取锁和释放锁的入口,子类全部都要遵守这个规则:
比如说要获取互斥锁要调用的是acquire(int arg)这个方法,释放互斥锁调用的是release(int arg)这个方法。
获取共享锁调用的是acquireShared(int arg)这个方法,释放共享锁调用的是releaseShared(int arg)这个方法。
对实际获取和释放的实现逻辑进行了定义,具体逻辑由子类实现
(1)tryAcquire(int arg):获取独占锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(2)tryRelease(int arg):释放独占锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(3)tryAcquireShared(int arg):获取共享锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做
(4)tryReleaseShared(int arg):释放共享锁的具体逻辑,AQS只是对方法进行了定义,没有实现,具体实现逻辑交给子类去做。
就是说子类如果要实现一个独占锁或者共享锁、或者是读写锁的功能就有下面几种情况:
(1)如果子类要基于AQS之上实现独占锁的功能,就要继承AQS,然后重写AQS的tryAcquire()、tryRelease()这两个方法。
(2)如果子类要基于AQS之上实现共享锁的功能,就要继承AQS,然后重写AQS的tryAcquireShared()、tryReleaseShared() 这两个方法。
(3)如果子类要基于AQS之上实现读写锁的功能,就是同时具备读和写两种锁,那么上面的tryAcquire()、tryRelease()、tryAcquireShard()、tryRelease() 这四个方法都要实现。
再给你画一副图来理解一下:
上面的acquire、acquireShared方法等,作为一个入口提供外层调用;实际上啊内部会调用对应的tryAcquire、tryAcquireShared方法,这些try开头的方法实际上才是去真正去获取锁的。
这里我就有点疑问了,AQS为啥要这么设计呢?直接设计acquire、acquireShared等方法作为抽象方法,让子类实现这些方法不就行了吗?为啥还有搞个tryAcquire、tryAcquireShared方法让子类去实现?
老王:说到这里啊,那就是AQS对模板方法的设计模式应用了,比如结合一个acquire方法内部的源码给你讲解一下:
public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 调用子类的tryAcquire方法实际获取独占锁 // 2.调用tryAcquire失败会执行到addWait方法 // 3.执行acquireQueued方法 acquireQueued( addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
看上面的源码acquire方法作为一个入口方法,它其实是一个模板方法,有下面的模板流程。
(1)流程1:调用子类的tryAcquire方法实际上争抢资源
(2)流程2:如果争抢资源失败则执行addWait方法
(3)流程3:如果争抢资源失败则执行acquireQueue方法
acquire作为一个入口方法,里面定义了一套通用的模板逻辑;同时具体获取资源的实现逻辑由子类tryAcquire方法去实现。
这样的模板方法的设计模式,让所有调用这个方法都走同一套模板流程;同时啊具体的实现方法tryAcquire又是在子类中,子类去实现,这样又能保证拓展性和多样性。
学了上面的acquire方法之后,其实acquireShared方法内部也是一样的,也是使用了一套模板方法去做,不信你看看:
public final void acquireShared(int arg) { // 1.调用子类的tryAcquireShared方法去获取共享锁 if (tryAcquireShared(arg) < 0) // 2. 获取失败则走到模板方法的第二个流程 doAcquireShared(arg); }
相当于acquire、acquireShared方法作为入口,定义了一套模板机制;
同时tryAcquire、acquireShared作为模板流程中的一环,由子类实现。这样能保证走同一套流程机制,同时子类又可以实现不同的逻辑,保证了多样性和拓展性。
其实AQS作为一个同步的基础框架,而不是一个具体的同步器的原因就在于这里,对获取锁和释放锁定义类一套模板流程,但是具体获取和释放锁的逻辑,没有提供实现,交给子类去实现。
因为不知道具体子类要实现什么样的锁或者什么样的同步工具,所以这里它只是定义了空的方法,由子类根据自己的需要去进行定制,从而实现各种各样的并发工具。
AQS提供的入口机制,模板机制,具体获取的逻辑由子类实现
2.2.4、Node节点和等待队列
它定义好了获取资源的入口、规定了由子类去实现具体的获取逻辑之后。如果一个线程获取资源失败会怎么样?好像是需要一套机制,告诉线程获取失败之后应该怎么做,是继续尝试获取呢?还是等待别释放了我再去获取?
所以AQS底层定义了两个数据结构,分别为Node节点和等待队列,其中等待队列为Node节点构成的一个双向列表。
(1)AQS规定了一套机制是当线程获取锁失败之后,会将线程信息封装在一个Node节点中,Node节点信息包含线程信息、要获取什么锁、线程当前处于什么状态等。
(2)然后将Node节点放入等待队列的尾部,让线程继续在等待队列中等待。
(3)同时针对于等待队列有一套机制,规定了等待队列的哪个位置的节点可以重试获取锁,以及针对共享锁怎么在等待队列中的节点进行共享锁传播等。
Node节点
static final class Node { // 共享锁模式,表示这个节点的线程要获取的是共享锁 static final Node SHARED = new Node(); // 共享锁模式,表示这个节点的线程要获取的是共享锁 static final Node EXCLUSIVE = null; // 节点状态,CANCELLED表示被取消 static final int CANCELLED = 1; // SINGAL节点,表示下一个节点等待自己唤醒 static final int SIGNAL = -1; // 处于CONDTION模式 static final int CONDITION = -2; // 处于共享锁的传播模式 static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; }
thread:表示当前节点的线程
prev:前一个等待节点
next:后一个等待节点
锁模式:SHARED表示等待节点要获取的是共享锁、EXCLUSIVE表示等待节点要获取的是独占锁
waitStatus节点的等待状态
CANCELLED(1):表示当前节点的线程已经被timeout超时或者中断了,处于该状态的节点不再去获取锁
SIGNAL(-1):表示后继节点需要当前节点唤醒,当该节点释放锁的时候发现自己的状态是-1的时候,需要唤醒下一个还在等待的线程。
CONDITION(-2):表示当前节点正在等待一个Condition条件,具体我们后面讲到ReentrantLock的Condition的时候再去剖析
PROPAGATE(-3):传播模式,在获取共享锁的时候,如果资源还有剩余,发现是-3传播模式,需要唤醒后续的节点。
上面的节点状态 waitStatus < 0 表示节点处于有效状态,waitStatus > 0 表示被取消了,无效。所以啊,AQS中很多情况也是直接使用waitStatus < 0 判断节点是否有有效。
nextWaiter:表示下一个也在等待Condition条件的节点,
也就是当线程获取锁失败的时候,线程会被封装成一个Node节点,然后放入到等待队列的尾部对吧。
然后这个Node节点就记录这个线程的一些基本信息,比如这个线程对象、当前要获取的锁是SHARED(共享)的还是EXCLUSIVE(独占)、线程的等待状态是什么(也就是当前线程获取资源之后需要干什么?
释放锁需不需要唤醒后续的线程,如果是共享锁需不需要将资源传播下去),等等的这些基本的数据。AQS作为一个框架,管理一些获取锁失败之后的线程,放在等待队列中,所以啊这些线程的一些基本数据它肯定是需要知道的,所以就设计了一个Node的类来方法这些线程的基本数据。
等待队列
老王:AQS中使用Node节点存储线程的基本数据,然后一个个的Node节点连接起来就形成了一个双向链表,也就是等待队列。
AQS使用两个指针来对这个等待队列进行管理,分别为:
Node head: head表示等待队列的头结点
Node tail: tail表示等待队列的尾节点
等待队列的结构给你画张图说明一下:
这个等待队列只是一个由Node节点构成的一个双向链表而已,同时AQS定义了两个指针,分别是head头结点指针、tail尾结点指针来管理等待队列。
我们结合上面讲过的acquire作为获取独占锁入口、tryAcquire实际获取实现、等待队列讲解一下获取锁失败的流程:
(1)首先线程根据AQS提供的获取acquire入口去获取独占锁
(2)acquire调用子类的tryAcquire方法尝试去获取锁,如果获取锁成功直接返回
(3)如果获取锁失败,则将当前线程封装成一个Node节点,放入等待队列中等待
只是我还有个问题,就是获取锁失败之后具体是如何加入等待队列的?以及在等待队列中什么时候再去竞争锁?
这些问题我们后面在对AQS进行底层的实现机制、源码剖析的时候都会一个一个讲解的;对AQS提供的每个机制都深入到源码级别的剖析。
但是啊,本章我们先整体上让你知道什么是AQS,内部有什么东西,整体上提供了哪些机制和功能,至于底层的实现和分析环节,我们在后面的章节会慢慢讲解的。
先继续,就在进入到AQS提供的下一个机制
Condition沉睡唤醒机制
之前讲解Synchronized的时候讲过synchronized的wait和notify机制,这个你还记得不?不记得的话要翻看一下之前讲过的章节哦,基础知识还是需要打牢一点的
当时讲解的wait和notify是控制线程之前沉睡和唤醒机制的,这个必须是整合synchronized一起使用的,因为底层依赖于monitor的waitset集合,这个我还是记得很清楚的。
AQS作为一个并发的基础框架,它也是提供了类似wait、notify的一套机制,这套机制就是通过Condition来实现的。
condition的await方法就类似之前讲过Object对象的wait()方法一样,具有一样的功能。会让线程释放锁,然后陷入沉睡等待,等待别的线程将它唤醒
condition的singal方法类似之前讲过Object对象的notify()方法,随机唤醒一个因为调用这个condition的await而陷入等待的线程;
而singalAll() 方法就类似于notifyAll方法,会唤醒所有因为调用这个condition的await而进入等待的线程。
这里啊,画个图类比一下condition和synchronized讲解的wait和notify功能:
上面那个图的意思我大概知道了,就是说AQS提供的Condition机制的功能和synchronized中的wait和notify功能一样呗。
如果不想使用synchronized和wait、notify;那么AQS也提供了类似的功能,直接使用AQS的功能是吧?但是底层Condition是怎么实现呢?
AQS提供了的await、singal功能和synchronized体系的wait、notify是一样的,作用也是一样的。
3、AQS的独占锁机制
首先啊,我们就从acquire和release入手,分析一下AQS为独占锁提供的机制:到底是怎么在获取资源失败进入等待队列的?以及释放资源的时候怎么唤醒后继节点的线程竞争锁的?
AQS竞争失败入队等待机制
首先我们看一下AQS获取资源的入口acquire方法的源码:
public final void acquire(int arg) { // 1.调用子类的tryAcquire方法,去获取锁 if (!tryAcquire(arg) && // 2.获取资源失败调用addWaiter方法插入等待队列 // 3.然后调用acquireQueued方法在队列实现阻塞或者再去获取锁 acquireQueued( addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
我们看一下上述的acquire这个模板方法,上面规定了几个模板流程:
(1)流程1:第一个是调用子类的tryAcquire方法去获取锁,如果获取成功则不用走下面的逻辑了,直接返回
(2)流程2:如果获取失败,则点调用addWaiter,封装互斥锁模式的Node节点进入等待队列
(3)流程3:进入等待队列之后,调用acquireQueued方法,是否需要阻塞等待,什么时候再去获取锁等,这些及具体的逻辑封装在这个方法里面。
接着看一下addWaiter方法是怎么将节点插入到等待队列的?
addWaiter方法源码解析
调用addWaiter方法,将节点加入等待队列尾部:
// node模式为互斥锁模式 addWaiter(Node.EXCLUSIVE), arg)
private Node addWaiter(Node mode) { // 创建一个新的Node节点,封装当线程线程 Node node = new Node(Thread.currentThread(), mode); // 获取等待队列的尾部节点 Node pred = tail; // 如果此时pred!=null,说明等待队列不是空,可以尝试插入新的尾结点 if (pred != null) { node.prev = pred; // CAS操作插入新的尾部节点,CAS操作让tail指针指向node节点 if (compareAndSetTail(pred, node)) { // 插入成功,修改一下node的prev指针 pred.next = node; return node; } } // 如果等待队列没初始化,或者插入失败则走到这里 // enq方法保证绝对将node节点插入为尾部节点 enq(node); return node; }
上面的流程大概可以归纳为如下几点:
(1)如果pre!=null,说明等待队列不是空,可以尝试cas把 tail 指向 node 节点,即cas插入一个新节点
(2)如果compareAndSetTail(prev, node)即cas将tail指针指向node节点成功,你需要修改一下prev指针,保持等待队列是双向链表,然后就返回了
(3)如果等待队列为空,即tail == null;或者cas操作失败了,则进入enq方法,保证一定会将节点插入到等待队列
enq(Node node)方法源码
private Node enq(final Node node) { for (;;) { // 获取tail节点 Node t = tail; // 如果t == null说明等待队列是空的 if (t == null) { // Must initialize // 等待队列是空的,必须要初始化一个空线程的Node节点 // 然后将head和tail都指向这个没有线程的节点 // 初始化好之后,在进入下一个循环,尝试将tail节点指向node if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // 如果等待队列不是空的 // 则CAS尝试将tail指针指向这个新的node节点, // 如果CAS成功了则说明插入队列尾部成功了,直接返回 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
说到这里我就有个疑问了,为啥尝试插入节点到尾部节点都要执行CAS操作,就是上面的那compareAndSetTail?
你想想啊,AQS同步工具,肯定是会存在多线程并发操作的情况啊,如果不执行CAS操作保证原子性,同时tail、head使用volatile修饰,保证可见性和有序性。这几点都保证了才能保证是线程安全的呀?
插入的操作要保证线程安全。
为啥如果等待队列是空的就需要先初始化一下,搞个空的节点作为头结点,搞一个没有线程对象的节点作为头结点?直接使用我们插入的那个节点作为头结点不行吗?
这个跟AQS获取锁机制的设计有关。AQS规定啊,头结点必须是已经获取获取锁的节点;或者头结点必须是一个空节点,即头结点是一个不再需要等待锁的节点。
然后等待队列中的第二个节点是等待队列中即将能够获取锁的节点,就比如下面图形:
也就是说AQS规定了老二节点是等待队列中下一个能获取锁的节点。如果插入发现等待队列是空的,于是就初始化一个空的节点,然后在插入,这样保证自己是老二。当别的线程释放锁的时候就轮到它了对吧。
老王:好了,我们将addWait()方法的源码就到这里,addWait其实就是将线程插入到等待队列中,我们接着上面的那个大流程继续讲解。
acquireQueue方法源码
我们先回到一下acquire方法的源码内部:
public final void acquire(int arg) { // 1.调用子类的tryAcquire方法,去获取锁 if (!tryAcquire(arg) && // 2.获取资源失败调用addWaiter方法插入等待队列 // 3.然后调用acquireQueued方法在队列实现阻塞或者再去获取锁 acquireQueued( addWaiter(Node.EXCLUSIVE), arg) ) selfInterrupt(); }
我们把addWaiter方法的源码和流程讲解完了,接下来就是acquire方法中的最后的一个模板流程,也就是acquireQueue方法就讲解acquireQueued方法的源码,我们接着看。
final boolean acquireQueued(final Node node, int arg) {
// 获取锁是否失败的标识,fail=true表示失败了,fail=false表示获取成功
boolean failed = true;
try {
// 中断标志
boolean interrupted = false;
for (;;) {
// 获取当前节点的前一个节点
final Node p = node.predecessor();
// 如果前一个节点p是head,则说明自己是老二节点
// 自己是老二节点则调用子类的tryAcquire再次竞争资源
if (p == head && tryAcquire(arg)) {
// 获取锁成功,将自己设置为头节点
setHead(node);
// 旧的头结点p可以被丢弃了,设置next=null,方便被GC
p.next = null; // help GC
// 获取锁成功了,所以fail自然就是false
failed = false;
// 返回被中断标识为false,表示获取锁成功了,没有被中断
return interrupted;
}
// 如果上面的操作没有成功;说明自己可能不是老二节点
// 或者自己是老二节点,但是tryAcquire方法获取锁失败了
// 这个时候就调用shouldParkAfterFailedAcquire方法判断自己是否需要被阻塞挂起
// 如果需要被挂起,则调用parkAndCheckInterrupted方法将自己挂起
// 挂起后如果被别的线程唤醒,然后继续执行,尝试去获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上面的源码可以使用画一个图来概括一下:
就是acquireQueued方法源码的核心流程图了,小陈你看懂了没?
小陈:我来捋一捋,其实就是说自己插入等待队列之后:
(1)会一直判断自己是不是老二节点,如果自己是老二节点则调用子类的tryAcquire方法争抢锁,如果争抢成功了,则将自己设置成头节点,原来的头结点则可以出队了。
(2)如果争抢失败了,则调用shouldParkAfterFailAcquire方法判断自己需不需要被挂起啊
(3)如果自己需要被挂起,则调用parkAndCheckInterruptd方法将自己线程挂起
(4)当别的线程释放锁,将自己唤醒之后,自己又重复上面的(1)、(2)、(3)步骤了
这就是acquireQueued方法内部的流程。shouldParkAfterFailAcquire方法源码
上面的shouldParkAfterFailAcquire这个方法是怎么判断线程是否需要被挂起的?
接下来继续看,shouldParkAfterFailAcquire内部的源码是怎么样判断的。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// pred为当前线程节点node的前一个节点
int ws = pred.waitStatus;
// 如果前一个节点的waitStatus = -1 ,即SINGAL的时候
// 自己就需要被挂起了,当pred节点释放锁的时候发现waitStatus为SINGAL
// 说明后面还有人等着我唤醒,则将自己的下一个节点唤醒
if (ws == Node.SIGNAL)
return true;
// 如果ws>0,说明是无效状态,pred节点已经被timeout超时或者中断了
if (ws > 0) {
do {
// 然后继续往前找,找到一个ws <=0 的有效节点
// 中间的哪些无效节点,全部删除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 设置node前面的一个节点为SINGAL
// 相当于告诉它,老哥我把你的节点状态设置成SINGAL了
// 这个一个信号,说明后面有人等着你唤醒呢,你释放锁的时候记得唤醒一下
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上面的源码我们可以再画图讲解一下:
(1)线程发现如果自己的前一个节点状态是 -1,说明已经告诉前一个节点一个信号了,这个信号就是等他释放锁的信号将我唤醒(也就是说前一个几点状态是 -1 的时候,它释放锁的时候会叫醒你,你放心睡觉好了)
(2)如果发现前一个节点是无效节点,如下图所示,就删除这个无效节点,然后继续往前找,直到找到waitStatus <= 0 的有效节点
(3)找到前一个有效节点之后,将前一个有效节点的waitStatus 设置成-1,相当于给它一个信号(相当于告诉你前一个哥们,让它释放锁的时候叫醒你,然后你就可以安心睡大觉去了)。当前一个节点释放锁的时候需要将我唤醒啊,就如下图所示:
(1)其实就是判断一下自己前面是不是存在waitStatus <= 0 的节点。如果前一个节点是waitStatus > 0的无效节点,则继续往前找,找的过程中删除遇到的无效节点。
(2)找到前一个有效节点之后,给前一个有效节点一个信号waitStatus = -1,告诉它,兄弟,我在后面等着你呢,等你释放锁的时候记得叫醒我,我先去睡觉去了。
继续接着看,acquireQueue流程的下一个源码。
parkAndCheckInterrupt方法源码
private final boolean parkAndCheckInterrupt() {
// 到这里直接调用LockSupport的park方法将线程挂起,线程就被卡在这里了
LockSupport.park(this);
// 走到这里了,说明线程被唤醒了,判断一下线程是否被中断了,返回线程的中断状态
return Thread.interrupted();
}
上面的源码就非常简单了:
(1)直接调用LockSupport的park方法将线程挂起来了
(2)当线程被唤醒的时候,继续执行走到Thread.interrupted,就是返回一下自己的中断状态。如果自己被中断了,则不能继续获取锁了
上面的流程看起来是挺简单的,但是我还有一个疑问,线程被中断之后会怎么办呢?
继续回到最开始的acquire方法源码:
public final void acquire(int arg) {
// 1. 调用子类的tryAcquire去尝试获取锁
if (!tryAcquire(arg) &&
// 2.获取失败调用addWaiter进入等待队列尾部
// 3.调用acquireQueue是否需要再尝试获取锁,还是在等待队列里面沉睡
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
你看看当tryAcquire返回false的时候会调用acquireQueued方法是吧。acquireQueued方法又是返回线程的中断标志。所以啊,如果被中断了就会进行selfinterrupted里面,就打断当前线程的运行了。
cancelAcquire方法源码
如果在acquireQueue方法里面等待的时候,可能由于啥原因,比如上面的中断等因素,导致线程获取锁失败怎么办?
这个时候啊,就需要将等待队列里面的这个节点进行移除了。你看acquireQueued源码里面有一段finally逻辑,也就是方法返回前都会执行的,这里会判断如果获取锁失败了,就移除等待队列里面的这个节点了。
我们进入cancelAcquire方法内部源码看一下:
private void cancelAcquire(Node node) {
// 如果node节点为null,说明已经被移除掉了,直接返回
if (node == null)
return;
// 设置node的thread为null,已经该节点已经无效了
node.thread = null;
// 这里不断修改prev指针,就是删除中途一样是无效的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 设置节点的状态为CANCELLED,即为1,为无效状态
node.waitStatus = Node.CANCELLED;
// 如果node节点是tail尾结点,直接设置tail为null,说明队列里面没有元素了
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果节点pred不是头结点
if (pred != head &&
// 并且能pred节点是singal或者能将pred节点设置为singal
// 这样让pred释放锁的时候唤醒后续线程
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
// 并且pred节点是有效的,即它的thread不是空
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 走到这里说明node节点前面没人在等待锁了,可能pred是头结点或者node前面的都是无效节点
// 这个时候直接唤醒node节点的下一个节点,让他去竞争锁了
// 嘿,兄弟,前面没人等了,你别睡了,该你去获取锁了
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
上面的这个啊,我同样给你画个图来讲解一下:
如果此时要移除图中黄色的节点:
找到无效节点删除后,然后寻找前一个有效节点,修改指针,修改有效节点的waitStatus = -1,告诉它后面还有人等着:
这个cancelAcquire方法的源码流程你理解没有?
如果要移除node节点,大概的操作就是找到node节点的前一个有效pred的节点,修改一下指针,将pred的next节点指向node节点的下一个节点。然后设置一下pred节点的状态是-1,告诉pred节点,你释放锁的时候要唤醒后面的节点。
上面的流程大概就是acquire方法内部的全部流程了,讲到这里acquire源码的全部流程就讲解完毕了,这里我们再总结一下:(1)acquire首先直接调用子类tryAcquyire方法去获取独占锁,如果获取成功了就直接返回了
(2)然后获取失败了会调用addWaiter方法将自己封装成一个Node节点插入到等待队列里面,这个上面我们已经进行了源码深度剖析了
(3)插入等待队里之后呢,在调用acquireQueued方法,需不需要沉睡,如果不需要会在一个for循环里面一直尝试去获取锁,这个地方我们上面也画图分析了,源码也分析了
(4)最后会进入一个finally代码块里面,判断如果获取锁失败了,要从等待队里里面移除了
这个acquire方法里面的核心源码和核心的流程机制老王你都讲解的很清除了,这里我已经理解了。
release方法源码解析
老王:好的,既然acquire方法是获取锁的,那我们接下来继续,讲解一下AQS释放锁release方法的源码是怎么样子的?
public final boolean release(int arg) {
// 1. 首先进来直接调用子类的tryRelease方法去释放锁
if (tryRelease(arg)) {
// 2. 如果释放锁成功,去到head头结点,然后去
// 唤醒head节点的下一个节点
Node h = head;
if (h != null && h.waitStatus != 0)
// 这里就是唤醒h节点的下一个节点的实际方法
unparkSuccessor(h);
return true;
}
return false;
}
首先啊,release方法作为一个模板方法,里面定义了几个模板的流程:
(1)流程1:第一步还是去调用子类的tryRelease方法去释放锁
(2)如果释放锁成功,直接找到head节点,唤醒head节点的后续节点,也就是唤醒老二节点。因为head节点是一个已经获取到锁节点、或者是一个空节点,是不再需要锁的,所以下一个等待锁的肯定是老二节点。
我们继续看unparkSuccessor方法的源码:
private void unparkSuccessor(Node node) {
// 如果node节点是状态是 < 0,这是一下node节点状态为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 这里是直接找到node节点的下一个节点s
Node s = node.next;
// 如果s是无效的节点,waitStatus > 0 或者s为null,继续往后找到
if (s == null || s.waitStatus > 0) {
s = null;
// 这里是从tail往前面找,找到一个有效的节点将它唤醒
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 这里就是直接调用LockSupport.unpark方法将s节点的所在线程唤醒了
LockSupport.unpark(s.thread);
}
上面的源码啊,其实不算复杂,还是比较简单的,我们再画一个图来分析一下:
这大致上就是release方法内部的源码和流程了,
这个release方法内部的源码和流程还是蛮简单的:
(1)首先就是调用子类的tryRelease方法去释放锁
(2)释放锁成功之后,调用unparkSuccessor(head)去唤醒head节点的下一个节点,由于head是头节点,head的下一个就是老二节点,所以就唤醒老二节点,让老二节点来获取锁。
(3)如果老二节点是无效节点,那就从tail 节点往前找 ,找到一个有效的节点来唤醒。
就是这样,release内部源码还是很清晰的,哈哈,相对acquire源码来说算是很简单的了。
我们最后画个图从整体上梳理一下acquire获取锁以及release释放锁的全流程:
讲到这里。AQS获取独占锁和释放独占锁的底层源码、核心流程全部分析完毕了。
AQS(JAVA CAS原理、unsafe、AQS)框架借助于两个类:
- Unsafe(提供CAS操作)------------------------------------《AQS之:Unsafe(java可直接操作内存(),挂起与恢复,CAS操作)》
- LockSupport(提供park/unpark操作)-------------------《Java多线程系列--AQS之 LockSupport》
Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。因此,LockSupport可谓构建concurrent包的基础之一。CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。先用两张表格介绍一下AQS。第一个讲的是Node,由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node里面有:
属 性 | 定 义 |
Node SHARED = new Node() | 表示Node处于共享模式 |
Node EXCLUSIVE = null | 表示Node处于独占模式 |
int CANCELLED = 1 | 因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收 |
int SIGNAL = -1 | 表示这个Node的继任Node被阻塞了,到时需要通知它 |
int CONDITION = -2 | 表示这个Node在条件队列中,因为等待某个条件而被阻塞 |
int PROPAGATE = -3 | 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 |
int waitStatus | 0,新Node会处于这种状态 |
Node prev | 队列中某个Node的前驱Node |
Node next | 队列中某个Node的后继Node |
Thread thread | 这个Node持有的线程,表示等待锁的线程 |
Node nextWaiter | 表示下一个等待condition的Node |
看完了Node,下面再看一下AQS中有哪些变量和方法:
属性/方法 | 含 义 |
Thread exclusiveOwnerThread | 这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者 |
Node | 上面已经介绍过了,FIFO队列的基本单位 |
Node head | FIFO队列中的头Node |
Node tail | FIFO队列中的尾Node |
int state | 同步状态,0表示未锁 |
int getState() | 获取同步状态 |
setState(int newState) | 设置同步状态 |
boolean compareAndSetState(int expect, int update) | 利用CAS进行State的设置 |
long spinForTimeoutThreshold = 1000L | 线程自旋等待的时间 |
Node enq(final Node node) | 插入一个Node到FIFO队列中 |
Node addWaiter(Node mode) | 为当前线程和指定模式创建并扩充一个等待队列 |
void setHead(Node node) | 设置队列的头Node |
void unparkSuccessor(Node node) | 如果存在的话,唤起Node持有的线程 |
void doReleaseShared() | 共享模式下做释放锁的动作 |
void cancelAcquire(Node node) | 取消正在进行的Node获取锁的尝试 |
boolean shouldParkAfterFailedAcquire(Node pred, Node node) | 在尝试获取锁失败后是否应该禁用当前线程并等待 |
void selfInterrupt() | 中断当前线程本身 |
boolean parkAndCheckInterrupt() | 禁用当前线程进入等待状态并中断线程本身 |
boolean acquireQueued(final Node node, int arg) | 队列中的线程获取锁 |
tryAcquire(int arg) | 尝试获得锁(由AQS的子类实现它) |
tryRelease(int arg) | 尝试释放锁(由AQS的子类实现它) |
isHeldExclusively() | 是否独自持有锁 |
acquire(int arg) | 获取锁 |
release(int arg) | 释放锁 |
compareAndSetHead(Node update) | 利用CAS设置头Node |
compareAndSetTail(Node expect, Node update) | 利用CAS设置尾Node |
compareAndSetWaitStatus(Node node, int expect, int update) | 利用CAS设置某个Node中的等待状态 |
上面列出了AQS中最主要的一些方法和属性。整个AQS是典型的模板模式的应用,设计得十分精巧,对于FIFO队列的各种操作在AQS中已经实现了,AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可。
参考:https://mp.weixin.qq.com/s/P30uHz7COw6oUeW4DLdxRA