文章目录
0.前言
0.1 为什么要学AQS
相信学过并发编程的同学对于AQS应该都不陌生,AQS全名叫做队列式的同步器AQS(AbstractQueuedSynchronizer),内部定义了很多锁相关的方法。
来看它的UML图和思维导图(这两张图来着公众号Hollis,这篇文章也很多借鉴):
由上面两张图可以看到,AQS几乎与大半个JUC有关。只要学会了AQS,锁(独占锁和共享锁、公平锁和非公平锁、重入锁和不可重入锁)、同步工具类都可以说不成问题了,甚至也可以自定义一个同步工具类出来。
0.2 怎么学AQS
先说一些所需的背景知识,学习AQS之前需要了解一个设计模式——模板模式,AQS正是使用了这个模式;对于一些锁的概念也需要了解:独占锁和共享锁、公平锁和非公平锁、重入锁和不可重入锁;对于同步工具类的一些基本使用也要知道。
AQS由三个部分组成:volatile int state
、线程同步队列、节点Node,AQS的操作(加锁、解锁等)其实就是对这三部分的组装完成的。不同的组装逻辑就实现了不同的功能。下面会对这三部分做出解释。
下面的讲解对这些组装逻辑进行讲解,我把AQS的各种实现分成两种模式,一种是独占模式(独占锁)、一种是共享模式(共享锁)。
那重入锁非重入锁和公平非公平锁呢?相比起上面两种,其实只是在实现过程中一小地方不同而已,就不具体分成一类了,当然也会讲到。
下面开始学习,讲解过程中也会贴一些源码出来,理解如果有误也欢迎大家一起讨论,这里即是一个分享也是做了一下笔记。
1.模板模式与AQS
没看过模板模式的小伙伴,可以看这个视频,5分钟就可以搞定,很棒的UP主:五分钟搞定模板模式。我们都知道,在模板模式里方法分成了三种,一种是规定好的模板方法,一种是子类需重写的自定义方法,还有一种是用来辅助其他两种方法的辅助方法,在AQS中我们也可以看到这三种方法的存在。
- 模板方法
- 自定义方法
- 辅助方法(这里只是简单举几个例子而已,不仅仅只有这些,甚至可以说这些辅助方法也是AQS中的一大难点)
getState()
:获取state的值setState(int newState)
:设置state的值compareAndSetState(int expect,int update)
:使用CAS设置state的值
AQS的方法大概就分成这三类,如果不先了解模板模式,在学习过程中会很混乱。举一个例子来更好了解下AQS的实现,比如ReentrantLock
中的AQS实现,打开ReentrantLock
,看它的源码,可以发现一个类叫做Sync
:
这个类和AQS的关系就是具体实现子类和模板类的关系,ReentrantLock
的锁的实现就是利用这个来实现,也可以去看看使用了AQS的同步工具类,它们的内部也都有这个类。
那这个类在ReentrantLock
中又干了什么呢?我们可以点开ReentrantLock
的lock()
方法,看一下它的源码,其实就是直接调用sync
里的方法而已,ReentrantLock
的其他API也一样,都是直接调用sync
,到这里应该可以明白了吧。
其实AQS和Sync隐藏了锁的各种操作的具体细节,使用的时候直接调用即可。由此我们也可以得出一个结论:要想实现一个同步工具类(基于AQS),就要在内部建一个内部类,继承AQS,重写自定义方法。
问题:为什么要写成包装内部类的形式,再去调用呢,直接使用不香吗,即直接使用
Sync
类。
(其实问题是我自己当时想出来的,答案也是自己想的,没有标准答案)
答案:这样的设计更加优秀,具有可扩展性,拿ReentrantLock举例,它的sync类只是实现了独占锁(排他锁)的功能,但是它写成了内部类的形式,它可以再用两个内部类继承他,实现了公平锁和非公平锁,扩展了功能
这里也扩展一下,我们知道ReentrantLock
有公平锁和非公平锁的功能,这个就是利用其余的两个Sync
,其实名字已经写在那里了,它们都继承了Sync
。
2.AQS的实现
2.1 state
state其实很难用语言去描述它是什么,因为它在不同的实现逻辑里有不同的叫法,比如在ReentrantLock中表示当前锁是否存在,1表示锁被使用了,0表示锁没被使用。我个人把它叫做同步状态,来看下他的源码,其实就是一个简单的 int 类型:
这里简单总结下,AQS各种实现的state的所表示的逻辑:
2.2 同步队列
AQS中维护了一个FIFO队列,这个队列的每一个元素就是一个Node,Node包含了与其绑定的线程的信息,下面会讲到。这个队列使用的存储结构是双向链表。
这个队列是含有头节点的,头节点表示获得锁的线程,后面的节点是排队的节点。注意:只有第二个节点(前驱节点是头节点的节点)才会尝试去获取锁,其他节点都会被挂起。注意这个队列并不是直接使用Java提供的数据结构类Queue,而是由一个双向链表实现的。
2.3 Node
没有成功获得锁的线程将会被包装成一个节点加入同步队列,Node节点绑定了一个线程,以及包含了等待状态等信息,来看看它的属性:
看不懂没关系,先对它大概有个了解即可,知道它里面有什么东西就行。如果你仔细看了上面那个图,你会发现好像存在两种队列:一个就是2.2的同步队列,其实还有一个等待队列。
AQS里面也维护了等待队列。在Object的监视器模型上,一个对象拥有一个同步队列与一个等待队列,而AQS拥有一个同步队列和多个等待队列。
这个等待队列比较特殊,和我们要讲的加锁解锁原理关系不大,而是和Condition有关系,这里就简单说明有这个东西即可,不具体展开了,有兴趣可以看这篇博客:AQS中的同步队列和等待队列
2.5 说明
下面就会讲AQS的各种具体实现了,是会根据源码来讲,源码其实并不简单,第一次接触逻辑还是很混乱的,所以再讲解过程中我也会做流程图,来帮助大家更好理解。
还有一点,在看下面的内容之前,请先记住AQS的设计模式,记住哪些方法需要重写,哪些是模板方法,一般来说模板方法都是没有带try的,带try的一般都是自定义方法。熟悉了这些方法的作用后,对于下面看源码会帮助很大。
在讲解之前,我觉得有必要介绍一下一种代码风格,因为源码就是按这种风格写的,当时我第一次看源码的时候觉得这种风格有点难接受,一下子没反应过来(或许是我太菜了)。
先模拟一个场景:有三个方法A()、B()、C(),返回值都是布尔类型,它们的交互逻辑是,如果A返回false,则执行B,如果B返回true,则执行C
正常写法
if(!A()){
if(B()){
C();
}
}
特殊写法
if(!A() && B() && C());
这样确实比较简略,可读性方面就见仁见智吧,刚开始可能不喜欢,反正我现在是喜欢上这种写法了,感觉挺高级的哈哈 -,-
还有一点就是我下面会一直讲到线程获取到了锁,这里的锁并不是真正意义上的锁,是我说顺口了,其实他可能就是改了state,当前获得同步状态的线程设置成当前线程。大家按自己意思理解即可。
3.独占模式——ReenrantLock的AQS实现
AQS有两种模式,独占模式和共享模式,所谓独占模式就是一次性只能给一个线程获得锁,或者说只能由一个线程执行,其他线程都阻塞。ReenrantLock正是这种锁,我习惯叫做独占锁,当然也叫做排他锁。
ReenrantLock也是可重入锁,所以它的AQS实现也是可重入锁的实现,同时ReenrantLock也有两种模式:非公平和公平,对应了非公平锁和公平锁的AQS实现,上面也有说过。
接下来将会演示两种锁的AQS实现:可重入的公平的独占锁和可重入的非公平的独占锁。
3.1 可重入的公平的独占锁
源码基本都在这几个类中反复横跳:AQS、ReenrantLock、Sync、FairSync
3.1.1 加锁
-
第一步
首先我们先看加锁,查看ReenrantLock的源码的
Lock()
方法,是直接调用Sync
的lock()
,而这个是一个抽象方法,由于现在我们探讨的是公平锁,所以我们看FairSync
的lock()
方法,这个方法又调用了AQS的acquire()
,并传参数1。这个1是什么意思呢?看前面state在ReenrantLock应该可以大概知道什么意思了。
"ReenrantLock的Lock方法" public void lock() { sync.lock(); } "sync的lock方法" abstract void lock(); "FairSync的lock方法" final void lock() { acquire(1); }
流程图大概如下:
-
第二步
现在开始AQS源码了,坐稳了!进入AQS的
acquire()
方法,这方法是模板方法,作用是获取独占锁。它的内容就是我上面所说的那种代码风格了。第一步是
!tryAcquire(arg)
,因为arg是参数1,所以也是!tryAcquire(1)
,这一步是尝试获得锁。
第二步是使用addWaiter(Node node)
,这个方法作用是线程获取锁失败后(tryAcquire返回false)把线程封装成节点,加入同步队列。
第三步是使用acquireQueued(addWaiter(NULL), 1))
,作用是使用死循环,将节点绑定的线程进行自旋,或者根据封装好的节点的前驱节点的状态(上面Node提到的waitStatus)做出相应的处理,一种处理是把节点的线程挂起,一种是修改节点的位置,具体操作看下面。
第四步是使用selfInterrupt()
,这个方法使用前提是acquireQueued(addWaiter(NULL), 1))
返回的线程中断结果是true,这个方法的作用是中断此线程。"AQS里的acquire方法,模板方法,作用是获得独占锁" public final void acquire(int arg) { //执行顺序: //1.!tryAcquire(1),尝试获取锁 //2.addWaiter(Node.EXCLUSIVE), 1),把线程封装成节点加入同步队列中 //3.acquireQueued(addWaiter(Node.EXCLUSIVE), 1)) //4.selfInterrupt() if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } "AQS的tryAcquire方法,自定义方法,需重写" protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
-
tryAcquire(int arg)
tryAcquire(arg)
就是我们需要重写的自定义方法。所以我们回到ReenrantLock
的FairSync
的tryAcquire(arg)
方法。 这个方法的作用就是来尝试获得锁,它在FairSync中的实现逻辑是判断当前state
是否等于0。如果不等于0,则表示锁被获取了,那再判断一下当前获得锁的线程是否等于本身的这个线程,这里是可重入锁的实现,如果等于则表示现在是锁的重入,设置state表示锁的重入次数。如果不等于该线程,则表示当前锁被别人占用了,直接返回fasle表示获得锁失败,执行下一步
addWaiter(Node node)
。如果等于0,则表示当前锁还没人使用,这时会使用
!hasQueuedPredecessors()
,这个方法是辅助方法,作用是判断当前同步队列中是否有别的线程在排队,如果有则进行锁重入的判断;如果没有则使用CAS修改state,这里其实就是0变1,也即锁的获取,然后再把当前线程设置为获取了锁的线程,最后返回false表示线程获得所成功,这时不会进入下一步addWaiter(Node node)
。hasQueuedPredecessors()
、compareAndSetState(0, acquires)
、setExclusiveOwnerThread(current)
这三个都是辅助方法,后面两个比较容易理解,hasQueuedPredecessors()
比较复杂一点,这里也不展开了,这里有一篇博客写的也很不错:AQS-hasQueuedPredecessors()解析
下面是tryAcquire的源码和流程图:
"FairSync的tryAcquire方法,作用是尝试获取锁" protected final boolean tryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); //获取state int c = getState(); //如果state=0,即锁没人使用 if (c == 0) { //执行顺序: //1.!hasQueuedPredecessors(),判断同步队列中是否有线程在排队 //2.compareAndSetState(0, acquires),使用CAS修改state //3.setExclusiveOwnerThread(current),把当前线程设置为获得锁的线程 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //判断线程是否等于获得锁的线程,这一步是可重入锁的实现 else if (current == getExclusiveOwnerThread()) { // 获得重入的次数 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); //设置state setState(nextc); return true; } //state!=0,即锁被使用了 return false; }
-
addWaiter(Node node)
这个方法是辅助方法,定义在AQS中,我们可以去AQS里查看,这个方法的作用是把线程封装成一个节点,加入同步队列中。注意一下,使用这个方法的前提是
tryAcquire()
返回false,即获取锁失败。在调用这个方法的时候,传的参数是
Node.EXCLUSIVE
,这个是定义在AQS的Node里面的一个常数,表示独占模式的节点的意思,其实值为NULL,所以实际上调用的方法是addWaiter(NULL)
。这个方法做了什么事情呢?首先将当前线程封装成一个节点,然后把这个节点加入同步队列尾部,这个加入有两种实现方式,都比较简单,这里就无需像
tryAcquire()
那样大幅度展开了。addWaiter()
里面也还含有其它两个辅助方法,分别是compareAndSetTail(pred, node)
和enq(node)
,第一个方法就是使用CAS把节点设置成尾节点。第二个方法也是同样效果,但是它使用的是死循环,即只有将节点设置成尾部节点才算成功,同时它也有初始化的效果,因为使用
enq(node)
的场景就是同步队列还未初始化。它的逻辑也比较简单易懂。"AQS中的addWaiter方法" private Node addWaiter(Node mode) { //新建一个节点,绑定线程,mode = null Node node = new Node(Thread.currentThread(), mode); //获取尾节点 Node pred = tail; //尾节点不为空,把当前节点加入同步队列尾部 if (pred != null) { //将当前节点的前驱节点设置为尾节点 node.prev = pred; //使用CAS把当前节点设置为尾节点 if (compareAndSetTail(pred, node)) { //将旧的尾节点的后继节点设置为当前节点 pred.next = node; //返回封装好的节点 return node; } } //尾节点为空,把当前节点加入同步队列尾部 enq(node); //返回封装好的节点 return node; } "AQS中Node的构造方法" Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } "AQS中定义的Node.EXCLUSIVE" static final Node EXCLUSIVE = null; "AQS中的enq方法" private Node enq(final Node node) { //死循环 for (;;) { //获取尾节点 Node t = tail; //尾节点为空,表示同步队列还未初始化,初始化队列 if (t == null) { //利用CAS设置同步队列的头 if (compareAndSetHead(new Node())) //并让尾节点指向头,此时队列只有头节点 tail = head; } //尾节点不为空,把当前节点放到尾节点的后继节点上 else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
-
acquireQueued(Node node,int arg)
这个方法也是AQS中的辅助方法,这个方法非常重要,作用是使用死循环,根据前驱节点状态,让线程去抢锁,对线程挂起或者修改节点的位置。有一说一,这个方法的逻辑有点点复杂。下面先介绍这个方法里面包含的其他几个辅助方法:
-
predecessor()
:这个方法比较简单,作用是获取当前节点的前驱节点 -
shouldParkAfterFailedAcquire(p, node)
:这个方法就是对前驱节点的状态进行操作,如果前驱节点状态是SINGAL
,则把线程挂起;如果前驱节点状态是CANCELLED
,则节点前移到状态不为CANCELLED
的节点后面;如果前驱节点状态既不是CANCELLED
也不是SINGAL
,使用CAS把前驱节点状态改成SINGAL
为什么要这么做呢?这一点很重要(⭐),在AQS中一旦加入了同步队列,线程就会被阻塞。那什么时候被唤醒呢?由前一个节点唤醒。所以选择好一个前置节点就很重要,否则这个节点要是已经退出等待的了,那你将永远没有醒来的一天。简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队, 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。
-
parkAndCheckInterrupt()
:这个方法的作用就是把线程挂起,线程会被阻塞,一旦被前置节点唤醒,就会检查当前线程是否已经被中断,返回线程的中断检查结果。 -
cancelAcquire(node)
:这个方法是把当前节点从队列中移除,会被移除的节点都是发生了异常且没拿到锁的节点,是不正常的节点,这个方法实现也挺复杂的,这里也不多展开了,有兴趣可以看这篇博客:cancelAcquire() 方法。
接下来讲下这个方法的逻辑,因为逻辑比较复杂,这里会比较简略说一下,大家主要看着源码和流程图自己跟一遍即可。
首先是初始化两个标记变量:一个是失败标记为true,代表意义是否线程发生异常(线程被挂起或者中断,继续对线程操作会发生异常),默认是会发生;另一个变量是中断标记,这个比较好理解,表示线程是否中断,默认为false,表示线程不会中断。
进入死循环,获取当前节点的前驱节点,判断前驱节点是否为头节点,如果是则尝试获取锁,这一点在上面的同步队列那里有说过。如果获取锁成功,则把当前节点设置为头节点,并且把失败标志改为false,表示线程不会发生异常,返回中断结果。
如果不是前驱节点或者获取锁失败,则对节点的前驱节点进行判断,然后做出相应操作,这个是
shouldParkAfterFailedAcquire(p, node)
和parkAndCheckInterrupt()
的逻辑,执行完成后回到开始,继续判断前驱节点是否为头节点。要退出这个死循环有两种方法:一种是线程获取到了锁,一种是
tryAcquire()
方法发生异常(具体是什么情况才会发生异常我不大清楚),注意一点:线程被中断是完全不会影响这个方法的,这个方法虽然涉及了对于中断的操作,但是完全没有做出相关处理,也就是说线程被中断了对抢锁结果无影响,但在另一个方法acquireInterruptibly()
里就有对中断的处理,下面也会讲到。退出后会根据失败标记进行失败操作的处理,这一步也很好理解,如果获取到了锁,那么就说明没失败所以不必执行失败操作(失败标记为false);如果是线程挂起或者中断发生异常退出的情况,则要执行获取锁失败的处理(失败标记为true),处理就是把当前节点从队列中移除。
源码和流程图如下:
"AQS中的acquireQueued方法,作用是使用死循环对节点线程进行自旋,或者根据前驱节点状态进行挂起或者修改节点的位置" final boolean acquireQueued(final Node node, int arg) { //失败标记默认为true(标识线程被中断或者发生异常,这里的异常一般是线程被挂起后发生的) boolean failed = true; try { //中断标记(标识线程是否被中断),默认未false boolean interrupted = false; //死循环 for (;;) { //获得当前节点的前驱节点 final Node p = node.predecessor(); //执行顺序: //1.p==head 判断前驱节点是否为头节点 //2.tryAcquire(1) 尝试获取锁 //这里的作用是判断前驱节点是否为头节点,如果是接着尝试获得锁,获得锁成功,则把当前节点设置成头节点 if (p == head && tryAcquire(arg)) { //设置头节点 setHead(node); //帮助垃圾回收,如果不懂垃圾回收什么意思可以不用理 p.next = null; //失败标记改为false failed = false; //返回中断标记 return interrupted; } //执行顺序: //1.shouldParkAfterFailedAcquire(p, node),这方法作用是根据前驱节点状态做出一些操作,具体看源码和流程图 //2. parkAndCheckInterrupt(),作用是将线程挂起,并返回线程的中断检查结果 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //中断标记标记为true interrupted = true; } } finally { //由于上面是死循环,所以只有两种情况才能到达这里: //1.节点的线程获取到锁了 //2.节点的线程被中断或者发生了异常(被挂起) //第一种情况不会执行下面的方法,因为它的failed是false //第二种情况才会执行下面方法,这个方法的作用是把节点从同步队列中取消 if (failed) cancelAcquire(node); } } "AQS中的predecessor()方法,作用是获取前驱节点" final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } "AQS中的shouldParkAfterFailedAcquire()方法,作用是根据前驱节点的状态对当前节点,进行一些操作" private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取前驱节点的状态 int ws = pred.waitStatus; //如果前驱节点是SIGNAL,返回true if (ws == Node.SIGNAL) return true; //如果前驱节点是cancelled,则把节点前移到状态不为cancelled的节点后 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } //如果前驱节点状态既不是cancelled也不是SIGNAL,则使用CAS修改前驱节点状态为SIGNAL else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } "AQS的parkAndCheckInterrupt方法,作用是将线程挂起,并返回线程的中断检查结果" private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } "AQS中的cancelAcquire方法,作用是把节点从队列中移出,下面不多讲解了" private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { unparkSuccessor(node); } node.next = node; // help GC } }
-
-
selfInterrupt()
这个方法也是一个辅助方法,这个方法很简单,执行的前提是
acquireQueued()
方法返回的线程中断结果是true,即线程中断了。这里对线程进行中断处理,中断处理可以看我的另一篇博客:深究线程状态及切换
下面截取一小段中断方法讲解:
"AQS中的selfInterrupt方法,作用是中断线程" static void selfInterrupt() { Thread.currentThread().interrupt(); }
OK了,这就是独占式的获取锁的实现,文字的讲述可能会感觉混乱,所以当自己去学习的时候,一定要画流程图才会看得懂思路。其实上述的内容也涉及一下接下来要讲的东西,比如响应中断的加锁、超时加锁以及节点的状态。(独占模式下,只要认识SINGAL和INITIAL就OK了,CANCELLED不用管)
OKK,现在tryAcquire()
方法讲完了,很恐怖对吧,不过也就只有这个比较复杂了,下面其实很大基础上只是一些小细节不一样而已,现在来看下这个方法的逻辑流程图:
3.1.2 解锁
解锁其实相比起加锁就简单很多了,如果你吃透了上面的加锁步骤,解锁是一点难度都没有。okk,现在我们来看一下解锁步骤,和加锁一样还是在一系列类中反复横跳。
首先是ReentrantLock
的unlock()
方法,它调用了AQS的release()
方法,然后AQS又调用了ReentrantLock
重写的tryRelease()
方法。具体流程图和源代码如下:
"ReentrantLock"
public void unlock() {
sync.release(1);
}
"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;
}
"ReentrantLock重写的方法"
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
明白了具体的调用逻辑后,现在就来具体看源码了,先来看一下ReentrantLock的Sync的tryRelease()
,这个方法做了什么呢?看下面源码解析和流程图。
"参数是1,releases是1"
boolean tryRelease(int releases) {
"计算state"
int c = getState() - releases;
"判断一下,如果持有锁的线程不是请求释放锁的线程,就让这个请求是否锁的线程有多远滚多远,不是你持有的还敢来申请释放"
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
"一个标志,这里其实就是和可重入有关的,表示是否释放锁"
boolean free = false;
"如果state等于0,则说明没有线程重入,释放绑定线程,并把标志置为ture"
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
"修改state"
setState(c);
"返回标志,如果有重入即state大于0,表示解锁失败,则返回false;如果没有重入即state等于0,表示解锁成功,返回true,"
return free;
}
下面我们来看下AQS中的release()
:这个方法调用了tryRelease(1)
方法,上面我们分析到,tryRelease(1)
返回的结果就是解锁的结果,如果返回的是false表示解锁失败,那么release()
也直接返回false;如果解锁成功,则会进入那个if语句里。还记得我们在加锁的时候,我们把线程挂起了吗,现在解锁了就要把线程唤醒,这里面 有个辅助方法unparkSuccessor()
,就是唤醒后置节点,如果后置节点其实是已经取消状态的没用节点,那就不必唤醒它们了,而是从队列中重新找一个适合的新节点用于唤醒。
具体看下源码分析和流程图:
"参数是1,arg是1"
public final boolean release(int arg) {
// 解锁成功
if (tryRelease(arg)) {
"获取头节点,此时的头节点即绑定了当前线程的节点"
Node h = head;
"判断一下,如果头节点不为空且头节点状态不等于0(初始状态),一般这个判断都会返回true;为0表示同步队列只有头节点一个,没有后置节点"
if (h != null && h.waitStatus != 0)
"唤醒后置节点"
unparkSuccessor(h);
"返回解锁成功"
return true;
}
"解锁失败"
return false;
}
"唤醒后置节点,这里的node是头节点"
private void unparkSuccessor(Node node) {
"获取节点状态"
int ws = node.waitStatus;
"节点状态小于0"
if (ws < 0)
"使用cas修改状态为0"
compareAndSetWaitStatus(node, ws, 0);
"获取后置节点"
Node s = node.next;
"如果后置节点为空且状态大于0(CANCELLED),说明后置节点是无用节点,需要清理"
if (s == null || s.waitStatus > 0) {
"后置节点置空"
s = null;
"从队列尾部向前找,找到一个可用的节点,选其作为后置节点"
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
"如果找到了后置节点,则唤醒后置节点绑定的线程"
if (s != null)
LockSupport.unpark(s.thread);
}
okk,现在来总结下整个解锁流程,看图:
3.1.3 总结
总结一下吧。
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
- 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
- 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。
下面属于回顾环节,用简单的示例来说一遍,如果上面的有些东西没看懂,这里还有一次帮助你理解的机会。
首先,第一个线程调用 reentrantLock.lock(),翻到最前面可以发现,tryAcquire(1) 直接就返回 true 了,结束。只是设置了 state=1,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,那世界就太太太平了,完全没有交集嘛,那我还要 AQS 干嘛。
如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?
线程 2 会初始化 head【new Node()】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
首先,是线程 2 初始化 head 节点,此时 headtail, waitStatus0
然后线程 2 入队:
同时我们也要看此时节点的 waitStatus,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus 没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire 这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus设置为 -1。
那线程 2 节点此时的 waitStatus 是多少呢,由于没有设置,所以是 0;
如果线程 3 此时再进来,直接插到线程 2 的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。
这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。