文章目录
0. 前言
0.1 AQS是什么
AQS,全名AbstractQueuedSynchronizer,翻译过来就是抽象队列同步器,实际上它是一个同步框架,实现了软件层面的同步机制。软件层面?什么意思呢?假如你学过高并发,你肯定对加锁这个概念不会陌生,也必定对synchronized
这个关键字有所了解,这个关键字就实现了锁机制,但从底层来看,这个关键字实际上是使用到了操作系统、硬件等机制。
而AQS,它由著名的Java并发大师Doug Lea编写,它使用一套复杂的逻辑构造了同步机制,这种同步机制完全是由代码层面的复杂逻辑一层一层组建完成,当然他也使用了一些底层机制来确保某些功能的实现。这个同步框架对比起有什么synchronized
实现的有什么好处呢?
好处有三个:
- 第一个系统开销低,
synchronized
利用的底层机制实现,性能开销比较大,AQS则是依靠代码的逻辑实现,当然如今使用的jdk版本已经对synchronized
做了很多优化,这个问题也不是很明显了。 - 第二个就是使用灵活,
synchronized
是自动的加锁解锁,而AQS则是手动地加锁解锁,这个能让我们实现某些特殊的需求,而且AQS还能响应中断和超时等待的形式获取锁。 - 第三个就是可扩展性,AQS本质上是个同步框架,既然是框架就有框架的作用,它允许我们利用它自定义一些同步工具,用于满足特殊场合的需求。
AQS在很多地方都有使用,AQS几乎与大半个JUC有关,我们熟知的ReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
、Semaphore
等都是基于AQS来实现的,所以在下面的内容里我也会根据这些类的源码来分析AQS。
下面我们开始真正来了解AQS,首先来看一下它的组成,它由四个部分构成(我个人自己分的),这个是学习AQS的重点和难点,AQS的同步机制的实现原理实际上就是,这四个部分的不断变化。结构图如下:
-
同步状态state
同步状态state实际上是一个整型变量,state其实很难用语言去描述它是什么,因为它在不同的实现逻辑里有不同的叫法,我们可以把它理解成一个标记,比如在ReentrantLock中表示当前锁是否存在,1表示锁被使用了,0表示锁没被使用,大于1表示锁的重入。
这里简单总结下,AQS各种实现的state的所表示的逻辑:
-
同步队列
同步队列也有一种叫法叫做阻塞队列,AQS名字里的那个Q指的就是同步队列,里面的元素都是下面要讲到的节点Node。有一点需要注意,虽然同步队列叫做队列,但实际上它并非真的使用到了Java里的数据结构
Queue
,而是使用了自己维护的链表,且是双向链表。结构图如下。
同步队列是干嘛的呢?可以把它看成是一个线程排队器,排队其实就是先进先出也就是FIFO队列,里面的每一个节点就代表了线程(当然不仅仅只有线程,还包含了一些其他信息)。阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列
同步队列还维护了两个变量——
head 和 tail
,分别指向链表头部和尾部。头节点表示获得锁的线程,后面的节点是排队的节点。注意:只有第二个节点(前驱节点是头节点的节点)才会尝试去获取锁,其他节点都会被挂起。 -
条件队列
条件队列也叫等待队列(不确定这种叫法对不对,有人也把同步队列叫做等待队列),它也和同步队列一样,使用Node作为元素,且它也不是使用了
Queue
,而是使用了自己维护的链表,但是是单向链表。这个队列较特殊,它实际上算不上AQS的组成,它只和
Condition
有关系,其他同步工具类和两个锁都没有涉及到这个队列。在AQS中,阻塞队列只有一个,但条件队列可以有很多个。条件队列头节点指向阻塞队列的尾节点。
怎么理解条件队列呢?怎么有了阻塞队列还有条件队列?我们不妨先想想,什么线程会进入阻塞队列?很简单,抢不到锁的线程。(这里的锁并非真的锁,而是同步状态state,这里抢不到锁也可以看成修改state失败)在某些高并发场合下,我们可能需要控制线程的执行顺序,所以这里就有两个需求:对于互斥的需求(资源只能一时间给一个线程访问)和对于逻辑的控制。而阻塞队列实现了对互斥的需求,那对逻辑的控制呢?很明显了,这个就可以利用条件队列来实现。所以条件队列是在同步队列的基础上,多了一层逻辑的阻塞。
当然,同步队列也可以实现对于逻辑的控制,比如各种同步工具类。
-
队列节点Node
队列节点Node,顾名思义就是两个队列里面的元素,或者说是两个链表里面的节点,它的组成比较简单,如下:
前三个元素很好理解,因为它是两个链表的节点,同步队列是双向链表,条件队列是单向链表,所以prev
、next
表示同步队列的前置节点和后置节点,nextWaiter
表示条件队列的后置节点。关键是
waitStatus
,这个表示节点的状态,这个就比较复杂。它有以下五种状态:-
SIGNAL
值为-1,这个是最常见的节点状态,表示唤醒后置节点。这里可能会有一些难理解,什么叫唤醒后置节点?首先我们要明确一点,就是线程一旦进入了阻塞队列,那么就表示线程被阻塞了。那什么时候被唤醒呢?由前一个节点唤醒。那又衍生出一个问题,前一个节点什么时候醒?
这个机制其实挺有趣的,同步队列中,只有一个醒着的节点,那就是头节点,它表示线程获取到了锁,只有头节点才会醒着,其他节点都是处于被挂起状态。当一个节点变成头节点后,它会判断自身的状态是否是SINGAL,如果是,他就会叫醒第二个节点(头节点的后置节点),让它去尝试获取锁。如果成功那他就变成新的头节点,然后重复上诉步骤。
-
CONDITION
值为-2,这个比较好理解,看名字我们也清楚是
Condition
条件队列有关的,当节点状态是CONDITION
的时候,表示它处于条件队列中。 -
PROPAGATE
值为-3,这个也是比较难理解的一个状态,它是属于AQS一种表现模式——共享模式(这个后面会讲到)才有的状态。这个状态比较特殊,表示下一次共享式获取锁将会无条件地传播下去。现在听不懂没关系,后面会讲到的。
-
CANCELLED
值为1,顾名思义这个表示取消排队状态,当一个线程在同步队列中排队时,会因为很多种情况而退出排队,比如发生了某些异常,这个时候就会把这个节点设置成
CANCELLED
,表示该节点已经取消了排队。然后在某些操作中,就会把这些已经取消排队的节点从队列中删除。 -
初始状态
值为0,当一个节点进入同步队列的时候,初始值就是0,它严格来说不属于AQS规定的一种状态,但是这里还是简单说明一下。也有一种情况会把节点状态设置为0,就是头节点执行完它的操作以后,下个节点要成为新的头结点时,就会把旧的头节点状态设置成0,然后把旧的头节点从链表中删除。
-
0.2 AQS怎么学
简述完AQS是什么后,我们现在来看下AQS要怎么学。AQS其实并不简单,AQS的学习,是有一定门槛的。首先我们需要有一些背景知识,如下:
-
高并发基本知识
AQS是个同步框架,大半个JUC包都使用了它,即使我们没了解过AQS是什么,但至少我们得清楚那些使用了AQS的类它们是干什么用的,同时对一些概念也需要有所了解,比如锁、同步、中断、等待超时等概念。
下文我对于AQS的讲解也是基于AQS的实现类的源码来讲,所以大家需要先提前了解一下使用到了AQS的类的大致功能是什么,这将会有助于你更好地了解AQS。如果你对这些都是很了解,但又想快速上手的话,可以先了解
ReentrantLock
、CountDownLatch
、Condition
这三个东西,因为下文将会通过解析它们的源码来讲解AQS。 -
设计模式
这个是一个必要的前提,AQS中使用到一种设计模式——模板方法模式,想深入了解AQS的同学必须要明白,这种设计模式也很简单很好理解,这里有一个视频关于模板方法模式的,没学过的小伙伴可以看一下: 五分钟学完模板方法模式——子烁爱学习,已经了解的同学可以往下看。
因为AQS使用了模板方法模式,所以我个人把AQS里的方法分为了如下三种:
-
模板方法:写在AQS里的普通方法,帮我们规定好了自定义方法和辅助方法的流程。这个也是我们学习AQS的主要内容。
-
自定义方法:写在AQS里的抽象方法,需要实现类去根据自身功能集体实现,可以看作是AQS同步框架暴露的接口,当然它的重写也需要遵守一定的规则。
-
辅助方法:写在AQS里的普通方法,这个要区别于模板方法,它起辅助作用,用于实现一些功能,可以把它们看作工具方法。比如以下这些,这里只是简单举几个例子而已,不仅仅只有这些,甚至可以说这些辅助方法也是AQS中的一大难点。
getState()
:获取state的值setState(int newState)
:设置state的值compareAndSetState(int expect,int update)
:使用CAS设置state的值
-
-
源代码编程风格
这一点怎么说呢,可能是我自己的原因,在我学习AQS之前,我的编程风格和AQS源码风格相差有点大,所以导致我刚开始看AQS源码的时候,觉得很吃力(是我太菜了-,-),所以在这里我也觉得有必要讲一下AQS的编程风格,以便于下面看源码的时候比较顺利。
先模拟一个场景:有三个方法A()、B()、C(),返回值都是布尔类型,它们的交互逻辑是,如果A返回false,则执行B,如果B返回true,则执行C
正常写法 if(!A()){ if(B()){ C(); } } 特殊写法 if(!A() && B() && C());
这样确实比较简略,可读性方面就见仁见智吧,刚开始可能不喜欢,反正我现在是喜欢上这种写法了,我的代码风格也慢慢变成了这种,个人感觉挺高级的。
-
AQS和实现类的结构关系
如果你是AQS的初学者,当你点击进去AQS的实现类的源码的时候,你会发现很奇怪的地方,就是你可能第一时间找不到哪里用到了AQS。然后你再仔细找一会,你就会发现每个AQS实现类都有一个内部类叫做
Sync
,而这个类继承了AQS,这里就用到了AQS。所以实际上,每个AQS实现类并非是直接继承AQS的,而是通过维护一个内部类
Sync
,然后重写AQS的自定义方法,然后通过组合这个内部类的各种操作来实现各种逻辑需求。这时你可能会有问题了。问题:为什么要写成包装内部类的形式,再去调用呢,直接使用不香吗,即直接使用
Sync
类(其实问题是我自己当时想出来的,答案也是自己想的,没有标准答案)
答案:这样的设计更加优秀,具有可扩展性,拿ReentrantLock
举例,它的sync
类只是实现了独占锁(排他锁)的功能,但是它写成了内部类的形式,它可以再用两个内部类继承他,实现更多的功能,比如它再用了两个内部类FairSync
和NofairSync
实现了公平锁和非公平锁,扩展了功能
0.3 AQS有什么用
看完是什么怎么学,现在来看看有什么用,那AQS有什么用呢?两个作用,一个是用于面试,一个是用于自定义同步工具类。个人认为第一个作用的意义大于第二个,因为已经存在的同步工具类实际上已经满足了工作遇到的业务需求了,基本无需自定义同步工具。
而第一个作用就很明显了,Java现在实在是太卷了,AQS应该算是必学的了吧,大厂面试应该是必问的,OKK讲完这些有的没的,下面我们就要开始真正的AQS学习了。
0.4 本文内容讲解
本文对于AQS的讲解将基于AQS的分类来讲,相信大家从文章目录就可以看出来了,AQS分为独占模式(互斥锁的实现)、共享模式(读写锁和同步工具类)、条件队列(condition),而这三类里面又有一些小分类,比如公平和非公平的实现、响应中断、超市等待等机制的实现,本文将一一讲解到。
独占模式将使用ReentrantLock
来讲解,共享模式使用CountDownLatch
来讲解,条件队列使用Condition
来讲解,请大家先了解下这三个东西是干什么的,这个将有利于下面的学习,同时也请大家了解下中断、超时等待这两个概念,这些可以在我的另一篇博客:线程状态切换
1. 独占模式
1.0 前言
独占模式也即互斥锁,表示资源在同一时间只能被一个线程所访问,最经典的实现就是ReentrantLock
。所以这里将会使用ReentrantLock
的源码来分析,但ReentrantLock
里面又有好几种不同的使用方式,比如公平锁和非公平锁,这个从源码就可以看出来,它里面有FairSync
和NofairSync
。
同时它也有一些响应中断的加锁方法,和超时等待的方法,实际上它们的过程都是大同小异的,只是在某些操作处做了不同的处理,所以下面的讲解会以公平且不响应中断的加锁解锁过程作为主要内容,然后讲完这个以后再具体分析非公平锁、响应中断的锁、超市等待的锁的实现。
1.1 公平不响应中断锁
1.1.1 加锁
1.1.1.1 方法总结
这个就是对加锁这个过程里出现的所有方法的总结,这里看不懂没关系,顺着下面的源码看就明白了,这里主要是起了一个便捷查询的作用,当你分析源码的时候,发现想不起来某个方法的作用了,就可以回到这里查找,方便记忆。
- 模板方法
void acquire(int arg)
:尝试获取锁,这种方式是公平地、且不响应中断地。参数一般是1,表示要获得的锁的数量。它规定了整个获取锁的流程。
- 自定义方法
boolean tryAcquire(int arg)
:尝试直接获取锁,这里的获取锁其实就是修改同步状态state,且把记录当前获取锁的线程改成当前线程,返回该过程的操作结果。返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取;返回false:锁被其他线程持有了。
- 辅助方法
boolean hasQueuedPredecessors()
:判断同步队列中是否有超过两个节点,因为只有头节点和第二个节点才会尝试去获取锁,其他都是被阻塞的,这一点我们在上面有讲过。Node addWaiter(Node mode)
:把线程加上一些必要信息,封装成节点Node,加入同步队列的尾部。Node enq(final Node node)
:这个方法是使用死循环,把节点加入同步队列尾部,这个可以看出是上面方法的一个保证,因为用到了死循环,所以一定会成功。boolean acquireQueued(final Node node, int arg)
:对同步队列进行调整,并返回线程在同步队列阻塞期间是否被阻塞boolean shouldParkAfterFailedAcquire(Node pred, Node node)
:对队列节点位置进行调整,判断节点绑定的线程是否应该被挂起,同时这个方法也会删除部分取消状态的节点boolean parkAndCheckInterrupt()
:把线程挂起,等到线程被唤醒的时候,清除并返回中断状态void selfInterrupt()
:中断线程void cancelAcquire(node)
:节点出错,有可能是tryAcquire方法报异常,也有可能是在响应中断的加锁方法中发生了中断,就会调用这个方法,这个方法清楚了状态:把绑定的线程设置为null,把节点状态改为取消状态,然后删除该节点
1.1.1.2 源码分析
"ReentrantLock的lock方法"
public void lock() {
"直接调用sync的lock方法"
sync.lock();
}
"sync的lock方法,是个抽象方法,需要具体实现,因为这里是公平锁,所以我们看FairSync的实现"
abstract void lock();
"FairSync的lock方法,调用acquire(1)方法,这个方法是aqs的模板方法"
final void lock() {
// 参数1表示每次要获取的锁的数量,也即每次state自增的数
acquire(1);
}
"AQS的模板方法,该方法作用就是整个加锁流程,且是公平地、不响应中断的"
public final void acquire(int arg) {
// 执行步骤:
/**
* 1. tryAcquire(1):
* 作用:直接尝试获取锁
* 参数:参数表示要获得的锁的数量,一般都是1
* 返回值:返回的结果就是获取锁的结果,返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
* 理解:在这里假如获取得到锁,就直接上锁了不必进行下面的操作;如果获取锁失败就要把线程进入同步队列排队阻塞
*/
/**
* 2. addWaiter(null):
* 作用:把当前线程和一些必要信息封装在一起,组成队列节点,加入同步队列
* 参数:Node.EXCLUSIVE表示独占模式,其实值是null,这个不必深究为什么,这个主要是和后面的共享模式做区分
* 返回值:封装好后已经加入队列的节点
* 理解:当到达这个方法时,表示线程直接获取锁失败,需要进入同步队列,所以需要封装成队列节点后加入队列
*/
/**
* 3. acquireQueued(node,1):
* 作用:根据队列的情况,对刚加入的节点进行相关操作,比如把节点绑定的线程挂起、比如让线程再次去尝试获取锁等等,下面会详解
* 参数:这个node就是封装好的节点,1表示获取锁的数量
* 返回值:线程是否中断
* 理解:队列的情况是十分复杂的,节点入队列的时候可能遇到很多种情况,这里就是针对这些情况对该节点做出相关处理。
* 这个方法十分复杂,它是整个加锁过程中最复杂的操作,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了。
* 如果线程不被中断的话,这个方法执行完也就说明拿到锁了,
*
*/
/**
* 4. selfInterrupt()
* 作用:把线程中断
* 理解:一般是不会进入这个方法的,假如进入则说明线程在排队的期间被中断了,这个方法会把线程的中断状态清除后,再中断一遍,相当于更新了一下中断
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 只有发生中断才会到达这里
selfInterrupt();
}
"ReentrantLock的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,1):
* 作用:cas修改同步状态state
* 参数:第一个参数是原来的值,第二个是要修改的值
* 返回值:修改结果
* 理解:这个方法比较简单,就是使用cas修改同步状态,如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
* 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了
*/
/**
* 3. setExclusiveOwnerThread(thread):
* 作用:修改线程为获得锁的线程
* 参数:当前线程
* 理解:这个方法比较简单,把当前线程修改为获得锁的线程,因为上面已经使用cas修改同步状态成功了,到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 表示获得锁成功
return true;
}
}
// 虽然state != 0,表示锁被获取了,但如果是它自己占用了锁,说明是重入了,需要操作:state=state+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 简单地判断
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置重入次数
setState(nextc);
// 表示获得锁成功
return true;
}
// 如果state != 0 且占有锁的线程不是该线程,表示锁被其他线程占有了,那它就获取锁失败
return false;
}
"AQS的addWaiter方法,是一个辅助方法,作用是把当前线程封装成节点,并加入同步队列"
private Node addWaiter(Node mode) {
// 参数mode此时是Node.EXCLUSIVE,代表独占模式,其实值是null
Node node = new Node(Thread.currentThread(), mode);
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
// 这个判断的意思是队列不为空
if (pred != null) {
// 将当前的队尾节点,设置为自己的前驱
node.prev = pred;
// 用CAS把自己设置为队尾, 如果成功后,tail == node 了,这个节点成为阻塞队列新的尾巴
if (compareAndSetTail(pred, node)) {
// 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了
pred.next = node;
// 线程封装的节点入队了,可以返回了
return node;
}
}
// 如果回到这里,说明队列是空的 或者 CAS失败(有线程在竞争入队)。所以就得调用下面的方法,下面的方法是利用死循环把节点入队,可以看成这是一个一定成功的方法,一定会把节点加入队列尾部
// 因为这个方法的本意就是要把节点加入队列尾部,CAS失败进入这个方法很好理解,因为多个线程同时操作可能失败,但是又要保证入队,所以就会使用这个方法。
// 那队列为空呢?你仔细看看,如果队列为空执行上面的操作,会报空指针异常,所以就加了一层判断
// 这种情况比较特殊,发生的原因大家可以先自行推测一下,这个有利于你们的学习
// 我的推测:有两个线程准备抢一个锁,这个锁是纯净的、刚new的,此时某线程tryAcquire(1)抢到锁,但是他没有加入同步队列的头节点!!!此时另一个线程抢不到锁就要加入同步队列,但此时队列是空的就是这种情况
enq(node);
// 返回封装好并入队的节点
return node;
}
"AQS的enq方法,辅助方法,作用是利用死循环把节点加入同步队列尾部"
private Node enq(final Node node) {
// 死循环在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的
for (;;) {
Node t = tail;
// 上面说过队列为空也会进来这里,这个判断就是队列为空的情况会进入的
if (t == null) { // Must initialize
// 初始化head节点
// 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的
// 还是一步CAS,你懂的,现在可能是很多线程同时进来呢
if (compareAndSetHead(new Node()))
// 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了
// 这个时候有了head,但是tail还是null,设置一下,
// 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了
// 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return
// 所以,设置完了以后,继续for循环,下次就到下面的else分支了
tail = head;
} else {
// 下面几行,和上一个方法 addWaiter 是一样的,
// 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排
node.prev = t;
// 这里有种情况很有趣,还记得上面进入这个方法有两种情况吗,有一种是队列为空的情况
// 这个时候你就会发现经过这个方法后会队列出现两个节点,而不是只有一个节点,这一点可以好好想想
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
"AQS的acquireQueued方法,辅助方法,根据队列情况对节点做出相关操作"
final boolean acquireQueued(final Node node, int arg) {
// 失败标志,表示在这个方法里面对节点做的相关操作,但却失败了,默认是失败的
boolean failed = true;
try {
// 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
// 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,
// 意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false
// interrupted 表示线程在同步队列里面排队的时候是否被中断了,一般不会被中断,所以默认为false
boolean interrupted = false;
// 进入死循环
for (;;) {
// 获取当前节点的前置节点
final Node p = node.predecessor();
// 如果前置节点是头节点,就去尝试直接获取锁,这里一再强调过了,只有前两个节点才会有机会尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功,把当前节点设置成头节点,这里也会把旧的头节点踢出队列
setHead(node);
p.next = null;
// 把失败标记改为false,表示没出错
failed = false;
// 返回中断情况
return interrupted;
}
// 如果到达这里则说明当前队列中超过了两个节点,所以线程进来就要被阻塞,下面执行的也正是把线程阻塞
// 执行步骤
/**
* 1. shouldParkAfterFailedAcquire(p,node):
* 作用:根据节点和前置节点的状态,做出相关操作,可以看成是对队列各节点的调整,这个比较复杂,具体看下面源码
* 参数:第一个参数是前驱节点,第二个参数才是代表当前线程的节点
* 返回值:是否应该挂起线程(这个结果是根据前置节点状态来决定的)
* 理解:这个方法和节点的挂起唤醒有关,从这个方法的名字来看,在上面讲节点状态的SIGNAL的时候,我们有说到线程都是上一个节点唤醒的
* 但是如果上一个节点出了问题怎么办(被取消排队了),那这个线程就永远都被挂起了,没有人叫醒它了,所以找到一个合适的前置节点很重要
* 这个方法正是做了这个操作,可以看成是对队列的调整,把当前节点加到合适的节点后面,然后挂起线程
* 这里返回的结果就是是否找到了合适的节点,找到了返回true,它可以安心去睡觉了,执行下面的挂起方法
*/
/**
* 2. parkAndCheckInterrupt():
* 作用:把线程挂起,然后被唤醒的时候,对线程中断状态进行判断
* 返回值:线程的中断状态
* 理解:这个就是线程的挂起方法,请注意线程执行了这个方法,假如没被唤醒的话,就阻塞在了这个方法里面了
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 到了这里,就说明线程在阻塞过程被中断了,中断标记设为true
interrupted = true;
}
} finally {
// 什么时候 failed 会为 true?tryAcquire() 方法抛异常的情况,什么情况会抛异常呢?我也没深究过-,-
if (failed)
// 这个方法就是实现了两个功能:
// 1. 把该节点从队列中删除
// 2. 清空状态,把节点绑定的线程置为NULL,把节点状态改为取消状态
// 详情:https://www.jianshu.com/p/01f2046aab64
cancelAcquire(node);
}
}
"AQS的shouldParkAfterFailedAcquire方法,辅助方法,判断是否应该挂起线程并对队列做出相关调整"
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前置节点状态
int ws = pred.waitStatus;
// 如果前置节点状态是SINGAL,则说明该节点合适,直接返回true,表示可以挂起线程了
// 合适的定义是前置节点释放的时候,会唤醒后置节点
if (ws == Node.SIGNAL)
return true;
// 前置节点状态大于0.其实只有一种情况就是取消状态,只有他的值是1是大于0的
if (ws > 0) {
// 这个循环的作用就是从队列的后面往前找,找到一个节点状态不是取消排队的合适节点,然后把当前节点排在合适节点后面,对队列做出调整
do {
// 这句话有无想起被C语言支配的恐惧
// 1.pred = pred.prev 2.node.prev = pred
// 这里也会删除取消状态的节点,在当前节点到找的第一个合适节点之间的取消节点会被删除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
// 如果节点状态既不是SINGAL,也不是取消状态,前置节点的状态可能为初始状态、或者共享状态,不可能是条件状态(这个后面会说,其实就是节点从条件队列转移到同步队列的时候节点状态会修改)
else {
// 老子不管了,我就要前面的人叫醒我,这里就直接把前置节点状态改为SINGAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 如果你前置节点不是SINGAL,你是在进了队列以后对节点位置做了调整,或者对前置节点状态做了调整,都会返回false
// 返回的结果可以理解成在没有经过调整之前,是否可以直接挂起线程
// 返回了false,就不会挂起线程,因为这个方法是位于死循环中(可以再去acquireQueued看一下),它很大概率上又会在进入该方法,这个时候经过调整,它的前置节点已经是SINGAL,直接返回true
return false;
}
"AQS的parkAndCheckInterrupt方法,辅助方法,挂起线程,被唤醒的时候返回中断结果"
private final boolean parkAndCheckInterrupt() {
// 把线程挂起,如果线程没有被唤醒则会被阻塞在这里
LockSupport.park(this);
// 注意哦,执行下面的操作则说明线程被唤醒了
// 清除线程中断状态,返回中断结果,这里把中断状态清除了,这也是假如发生了中断为什么还要再中断一次的原因
return Thread.interrupted();
}
"AQS的selfInterrupt方法,辅助方法,中断当前线程"
static void selfInterrupt() {
// 进入这个方法则说明上面线程被唤醒了,且线程在阻塞的时候被中断了
// 因为中断状态被清除了,这里要再中断一次
// 所以可以看到加锁是不响应中断的,线程被中断了,它也只有醒来才会发现被中断了,然后对中断也不报错也不处理
Thread.currentThread().interrupt();
}
1.1.1.3 流程图
-
方法调用流程图
-
逻辑流程图
1.1.2 解锁
1.1.2.1 方法总结
- 模板方法
boolean release(int arg)
:AQS的模板方法,规定了整个解锁流程,比起加锁流程简单了许多。
- 自定义方法
boolean tryRelease(int releases)
:尝试直接解锁,实际上就是尝试把state改成0,把获得锁的线程改为null,当然也考虑了重入锁的情况,如果是重入锁,则把state-1。
- 辅助方法
void unparkSuccessor(Node node)
:这里的参数node是头节点,这个方法就是找到头节点后面第一个适合唤醒的节点(有些节点可能取消了排队),唤醒该节点绑定的线程
1.1.2.2 源码分析
"ReentrantLock的unlock方法"
public void unlock() {
// 调用sync的release方法
sync.release(1);
}
"AQS的release方法,模板方法,规定了解锁的整个流程"
public final boolean release(int arg) {
// 尝试直接释放锁,返回释放情况
if (tryRelease(arg)) {
// 获取头节点
Node h = head;
// 头节点不为空且头节点状态不为0
if (h != null && h.waitStatus != 0)
// 叫醒头节点后适合醒来的第一个线程
unparkSuccessor(h);
// 返回true,解锁成功
return true;
}
// 返回false,解锁失败
return false;
}
"AQS的tryRelease方法,自定义方法,尝试直接解锁,其实就是修改同步状态state变为0修改获得锁的线程为NULL,也有考虑重入锁的情况,把state-1"
protected final boolean tryRelease(int releases) {
// 获取state,并计算减去1后的值,这里的releases表示要解的锁的数量
int c = getState() - releases;
// 判断当前线程是否是获得锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
// 如果不是,那你还敢来申请解锁,有多远滚多远,直接报错
throw new IllegalMonitorStateException();
// 是否完全释放锁(重入锁的问题)
boolean free = false;
// 如果state减去要解开的锁的数量,变成了0,说明完全释放了锁
if (c == 0) {
// 标记设置为true
free = true;
// 把获得锁的线程设置为NULL
setExclusiveOwnerThread(null);
}
// 修改state
setState(c);
// 返回释放锁的结果
return free;
}
"AQS的unparkSuccessor方法,辅助方法"
private void unparkSuccessor(Node node) {
// 获取头节点的状态,参数NODE是头节点
int ws = node.waitStatus;
// 头节点状态小于0,可能是SINGAL(-1)或者共享模式
if (ws < 0)
// 修改成初始状态0
compareAndSetWaitStatus(node, ws, 0);
// 下面的代码就是准备唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1),所以得找一个需要被唤醒的节点
Node s = node.next;
// 如果第二个节点为空或者取消了排队,就要找别的节点
if (s == null || s.waitStatus > 0) {
s = null;
// 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 找到了合适的节点,把该节点绑定的线程唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
1.1.2.3 流程图
- 方法调用流程图
- 逻辑流程图
上面已经讲完公平锁和不响应中断锁的实现了,其实也不难对吧,也不要担心,上面几乎就是独占模式的全部了,下面只是有一些小细节不一样而已。OKK,接着看其他机制锁的实现吧。
1.2 非公平锁
ReentrantLock 默认采用非公平锁,除非你在构造方法中传入参数 true。
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
-
公平锁
static final class FairSync extends Sync { final void lock() { acquire(1); } "AQS.acquire方法" public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { "1. 和非公平锁相比,这里多了一个判断:是否有线程在等待" 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"); setState(nextc); return true; } return false; } }
-
非公平锁
static final class NonfairSync extends Sync { final void lock() { "2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了" if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } "AQS.acquire方法" public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) { "这里的tryAcquire方法实则上是调用了nonfairTryAcquire方法" return nonfairTryAcquire(acquires); } } 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; }
公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
1.3 响应中断
"ReentrantLock的响应中断的加锁方法"
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
"AQS的acquireInterruptibly方法,模板方法,相当于普通加锁的acquire()"
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 开始加锁前,就会判断一次是否中断,是的话直接报错
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
"AQS的doAcquireInterruptibly方法,辅助方法,等同于普通加锁的acquireQueued()"
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 不要很疑惑在上个方法中,为什么没有把线程封装成节点加入队列,这个方法在这里
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 既然响应线程中断,那这里就不只是简单改下标志啦,直接报错
throw new InterruptedException();
}
} finally {
// 此时就不当当只有tryAcquire报异常会删除节点了,被中断了也会
if (failed)
cancelAcquire(node);
}
}
上面就是响应中断获取锁的方法源码,在此之前我们对于Locksupport.park()
这个方法还不了解的同学,可以先去看一篇博客:Locksupport与中断,从这篇博客我们可以总结出两点:
interrupt()
这个方法可以把被挂起的线程唤醒- 挂起的线程被中断唤醒后,如果中断标志是true,则再次挂起会失效
如果你明白了这两点,那么AQS中的中断机制就很理解了:
- 对于第一点,当我们使用
acquireInterruptibly()
方法的时候,把线程中断了,线程就会醒来,然后直接抛出InterruptedException,所以这样就实现了线程在同步队列中排队的时候,我们不想他排队了,直接中断他即可。因为它抛出了异常,所以会被try-catch捕获,然后因为失败标志还没改,依然是true,所以执行cancelAcquire()
方法,把该节点从队列中删除。 - 对于第二点,这个就和AQS不响应中断的加锁有关,你当初是不是很疑惑为什么在
parkAndCheckInterrupt()
和acquireQueued()
中为什么把中断状态要清粗,为什么要维护一个中断标志,答案就在这里了。因为中断标志不清除,一直为true,会影响以后的线程挂起。所以得手动维护一个中断标志,以及每次被中断唤醒的时候要清除中断状态。
1.4 超时等待
"ReentrantLock的tryLock方法"
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
"AQS的tryAcquireNanos方法,模板方法,等于普通加锁的acquire"
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
"AQS的doAcquireNanos方法,辅助方法,等于普通加锁的accquireQueue"
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果传参小于0,直接拿锁失败
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 计算剩余时间
nanosTimeout = deadline - System.nanoTime();
// 时间到,还没拿到锁返回false
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
// 剩余时间不是很小的话,可以把线程挂起
nanosTimeout > spinForTimeoutThreshold)
// 把线程挂起指定时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
等待超时的机制很简单,只是利用时间类做了一些操作和利用了Locksupport的可以指定挂起时间的机制,大家看看源码就明白了。
1.5 补充:尝试获取锁
"ReentrantLockd的tryLock方法"
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
"AQS的nonfairTryAcquire方法,自定义方法,相当于普通加锁的tryAcquire"
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;
}
这个机制也十分简单,其实就是直接调用了一个自定义方法,直接尝试获取锁,而不调用后续的辅助方法,这样就实现了尝试获取一次锁,若失败直接返回,不进行同步队列的排队。
2. 共享模式
2.0 前言
相比起独占模式,共享模式十分简单,可以把共享模式理解成独占模式的扩展。它的原理有很大部分是独占模式的内容,所以必须充分了解独占模式后再来了解共享模式。
关于共享模式,很重要的一点是它是AQS可扩展的主要部分。什么意思呢?AQS是一个同步框架,我们可以自定义扩展它,但大多数情况下我们的自定义扩展都是扩展共享模式的,对于独占模式一般都无需扩展。从它的实现类的数量就可以看出来了,独占模式只有ReentrantLock
和读写锁的写锁,而共享模式实现类有很多的同步工具类。
下面对于共享模式的讲解,将会基于CountDownLatch
,请各位小伙伴先去了解他大致功能是什么:
2.1 源码分析
"构造方法"
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 老套路了,内部封装一个 Sync 类继承自 AQS
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
// 这样就 state == count 了
setState(count);
}
...
}
代码都是套路,先分析套路:AQS 里面的 state 是一个整数值,这边用一个 int count 参数其实初始化就是设置了这个值,所有调用了 await
方法的等待线程会挂起,然后有其他一些线程会做 state-- 操作,当 state 减到 0 的同时,那个将 state 减为 0 的线程会负责唤醒 所有调用了 await
方法的线程。都是套路啊,只是 Doug Lea 的套路很深,代码很巧妙,不然我们也没有要分析源码的必要。
对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown()
方法,另一个是 await()
方法。
countDown() 方法每次调用都会将 state 减 1,直到 state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候,await 方法才会返回。await 可以被多个线程调用,读者这个时候脑子里要有个图:所有调用了 await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。
void await()
:阻塞方法,其实就是获取共享锁的方法public void await() throws InterruptedException { // 调用AQS的共享模式加锁,这个方法是响应中断的 sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 老套路了,先判断中断 if (Thread.interrupted()) throw new InterruptedException(); // 这个方法是自定义方法,具体逻辑在不同实现类中都不相同,尝试获取锁,获取锁失败就进入同步队列中 if (tryAcquireShared(arg) < 0) // 获取共享锁,是响应中断的 doAcquireSharedInterruptibly(arg); } // 这个方法尝试去获取锁,我们要自定义工具类其实就是自定义这个方法而已 // 尝试去获取锁(共享模式) // 负数:表示获取失败 // 零值:表示当前结点获取成功, 但是后继结点不能再获取了,countdownlatch没有这个,读写锁中就有 // 正数:表示当前结点获取成功, 并且后继结点同样可以获取成功 protected int tryAcquireShared(int acquires) { // countDownLatch的实现,很简单,判断state是否等于0 return (getState() == 0) ? 1 : -1; } // 有无发现和acquireQueue方法很像 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 加入队列,注意参数,和独占模式做个对比 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); // 前面说个这个返回值,进入这里面说明获取共享锁成功,1表示后面的节点也可以获得资源,0表示只有当前线程可以获得 // 在独占模式中,获取到了锁就只是把节点改成头节点而已 // 在共享模式中,获取到了锁还要尝试一下释放掉锁然后唤醒后置节点(共享就是这么实现的,一直传递下去唤醒下个节点,就实现了共享) if (r >= 0) { // 只有这里和独占模式有所不同,共享锁的所有难点所在 setHeadAndPropagate(node, r); p.next = null; failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } private void setHeadAndPropagate(Node node, int propagate) { // 获取头节点 Node h = head; // 把当前节点改为头节点 setHead(node); // 这里就是共享锁的精髓所在,之所以共享就是因为当一个节点被唤醒的时候,它会判断后续节点是否也为共享节点,是的话也去唤醒它 // 这样一直传递下去就实现了共享 // propagate就是tryAcquire的结果,>0表示后面的节点也可以获得资源,唤醒他 // 其他条件也可以发生释放共享锁的情况,为什么要这么做后面会将 // 1.老节点为空或者老节点状态小于0 2.新节点为空或者新节点状态小于0 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 下一个节点为空或者下一个节点是共享节点,释放共享锁 if (s == null || s.isShared()) // 释放锁并唤醒后置节点,这里就看下面的countDown()方法了 doReleaseShared(); } }
void countDown()
:倒数方法,其实就是释放锁的方法public void countDown() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { // 尝试释放锁,释放成功就去唤醒后置节点和对队列做出一些调整 if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } // 尝试释放锁,自定义方法 protected boolean tryReleaseShared(int releases) { // 这里很简单,就是使用cas对state-1,因为state在这里代表倒数的数 // state-1 == 0就表示成功了 for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } private void doReleaseShared() { for (;;) { Node h = head; // 队列不为空 if (h != null && h != tail) { int ws = h.waitStatus; // 头节点是-1,直接唤醒后置节点,并把头节点改为0 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h); } // 如果头节点为0就把他改成PROPAGATE else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head) break; } }
如果只是简单看完上面的源码,你或许会有很多疑问,共享的原理我能懂,就是不停地唤醒后置节点,但节点状态PROPAGATE有什么用呢?setHeadAndPropagate
方法到底做了什么?看下面的两篇博客,你可以找到答案。我先给出答案,其实就是为了解决某种情况下节点会被卡死在队列中的情况。具体看下文两篇博客:
AQS : waitStatus = Propagate 的作用解析 以及读锁无法全获取问题: https://www.cnblogs.com/lqlqlq/p/12991275.html
AQS源码深入分析之共享模式-你知道为什么AQS中要有PROPAGATE这个状态吗?: https://blog.csdn.net/tomakemyself/article/details/109499230
3. 条件队列
3.0 前言
下面我们将会讲解条件队列——Condition Queue
,条件队列有一个应用就是Condition
这个同步工具类,下面我们将会基于Condition
的源码进行讲解。但是你如果去搜索Conditon
,你会发现你找到的Condition
仅仅是一个接口,具体实现类在AQS中,叫做ConditionObject
,下面讲述的所有方法都来源于ConditionObject
。
在学习条件队列之前,需要大家对Condition
的用法有所了解,下面也将会基于Condition
的两个最基本的方法,等待方法await()
和唤醒方法sigal()
来讲解。
ConditionObject
的实现虽说是在AQS里面的,但我们实际用到的Condition
是基于ReentrantLock
实现的,下面讲源码的时候大家就会发现,有很多利用到了ReentrantLock
的实现方法。
在讲解源码之前,还是有几点需要和大家说一下:
- condition 是依赖于 ReentrantLock 的,不管是调用 await 进入等待还是 signal 唤醒,都必须获取到锁才能进行操作。
- 每个 ReentrantLock 实例可以通过调用多次 newCondition 产生多个 ConditionObject 的实例,每个ConditionObject 的实例管理一个条件队列
- ConditionObject 只有两个属性 firstWaiter 和 lastWaiter,且条件队列是单向链表
- 执行步骤
- 如线程 1 调用
condition1.await()
方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行 - 调用
condition1.signal()
触发一次唤醒,此时唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter(队头) 移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行
- 如线程 1 调用
3.1 方法总结
void await()
:等待方法,把线程阻塞在条件队列中,直到被sigal
方法唤醒为止,或者线程被中断Node addConditionWaiter()
:把线程包装成节点,加入条件队列,节点状态初始化为CONDITION,同时在加入过程中会对条件队列做出调整,返回该节点。void unlinkCancelledWaiters()
:删除条件队列中所有非CONDITION的节点int fullyRelease(Node node)
:完全释放锁,并返回释放的锁的数量,有可能解锁失败,直接发生异常。boolean isOnSyncQueue(Node node)
:判断当前节点是否存在于条件队列中boolean findNodeFromTail(Node node)
:从同步队列往前找,判断当前节点是否存在于同步队列中
int checkInterruptWhileWaiting(Node node)
:检查线程的中断状态,返回值0表示没中断,1表示sigal()
后才发生中断,-1表示sigal()
前发生了中断。且会把中断的节点转移到同步队列中。boolean transferAfterCancelledWait(Node node)
:判断是发生了什么情况的中断,如果是sigal前发生的中断,会修改节点状态,还会把节点转移到同步队列,但不会从条件队列中删除。
void reportInterruptAfterWait(int interruptMode)
:对不同的中断情况做出处理,sigal前报异常,sigal后重新中断。
void signal()
:唤醒方法,把节点从条件队列中转移到同步队列里面去void doSignal(Node first)
:找到合适的节点,去转移节点,从条件队列转移到同步队列中boolean transferForSignal(Node node)
:修改节点状态,转移节点,某种情况下,还能主动唤醒线程
3.2 源码分析
这里我们就不再区分等待方法和唤醒方法了,我们直接放在一起讲,因为Condition
的唤醒和等待方法的逻辑有很多地方是交互的,如果分开来说可能会有点混乱,这里就放在一起,按照等待——唤醒的过程一步步分析,中途也会说一下可能发生的其他情况。
-
void await()
首先来看下
void await()
方法,这个方法是响应中断的,不响应中断的是void awaitUninterruptibly()
,这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll()),或被中断。下面就是源码分析,里面有一些注释暂时看不懂没关系,你先记住一个大概流程,然后调用到了什么方法,这些方法大概作用是什么就可以了。public final void await() throws InterruptedException { // 响应线程中断,所以在一开始就判断线程是否中断,老套路了 if (Thread.interrupted()) throw new InterruptedException(); // 把节点包装好加入条件队列,节点状态为CONDITION,某种情况下,这个方法会清除队列中所有删除状态节点 Node node = addConditionWaiter(); // 释放该线程拥有的锁,这里要注意一下,假如是重入锁也就是线程拥有多把锁,这里也要释放干净 // 这里也要记录下拥有的锁的数量,因为一旦这个线程被唤醒了,它也要获得原来的锁 // 注意!!!这个方法是有可能报出异常的,表示释放锁失败,节点会被设置成取消状态,且到这里就结素了不会执行下面的操作 int savedState = fullyRelease(node); // 初始化中断标志为0,这里先讲一下这个标识是什么意思 // 值为0,表示没有中断,默认如此 // 值为-1(THROW_IE),表示在被sigal唤醒之前就被中断了,这表示会报出中断异常 // 值为1(REINTERRUPT),表示在被sigal唤醒之后才被中断,这表示会需要重新设置线程的中断标志 // 思考:为什么中断还要分成sigal前和sigal后呢 // 因为await这个方法是响应中断的,如果是在await期间被中断就要报中断异常,如果是在await后被中断那它才不管,直接设置中断信号就好 int interruptMode = 0; // 进入循环,线程就在这个循环中被挂起了,退出循环有两个条件: // 1.线程进入了同步队列,即线程被sigal唤醒了 // 2.线程被中断 // isOnSyncQueue就是判断线程是否进入了同步队列 while (!isOnSyncQueue(node)) { // 挂起线程 LockSupport.park(this); // 获得中断标记,checkInterruptWhileWaiting这个方法不仅会检查线程的中断,如果发生了中断还会把节点转移到阻塞队列里 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 这里的作用是去加锁,此时节点已经被转移到同步队列里了!!! // 注意,这里是加锁n次哦(n == savedState,即原来持有的锁的数量) // 还记得acquireQueued返回值代表什么意思吗?表示线程是否在加锁过程中被中断了,所以这里如果中断了,也会把中断标志改为REINTERRUPT,表示需要重新设置中断 // 进入这里可能有三种情况: // 1. 中断标志值为-1,也就是THROW_IE,表示sigal之前发生了中断 // 2. 中断标志值为0,表示没发生中断,这个时候就去获取锁,如果获取锁过程中发生中断,就把中断标志设置为REINTERRUPT // 3,中断标志值为1,表示REINTERRUPT,注意!!此时表示线程在sigal后发生中断,但再详细点说其实是在从条件队列转移到同步队列过程中,发生了中断 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // 理解:REINTERRUPT表示在sigal后发生了中断,详细来说其实是分两种情况 // 1.如果到达if之前的时候中断标志已经是REINTERRUPT,表示在节点转移的时候发生了中断 // 2.如果是经过下面这句再修改成REINTERRUPT的话,表示线程在同步队列中获取锁时发生了中断 // 但是两者都是sigal后发生中断的,对这两种情况的处理并没有什么区别,所以就都设置成REINTERRUPT interruptMode = REINTERRUPT; // 调整队列,这里仅针对sigal之前发生中断的情况 // 在checkInterruptWhileWaiting中,会把sigal中断前的节点状态设置为0,且加入同步队列,但要注意!!!它并没有把节点从条件队列中删除 // 所以这里要删除一下,因为节点状态已经给改成0,不是condition了,所以会被删除 if (node.nextWaiter != null) // 这个方法的作用是清空条件队列中所有节点状态不为CONDITION的节点 unlinkCancelledWaiters(); // 如果线程发生了中断,针对不同的中断情况做出处理 if (interruptMode != 0) // 其实也很简单,sigal之前直接抛出异常,sigal之后重新设置中断标志并再中断一次,为什么要重新设置在上面讲响应中断的加锁的时候应该有讲过了 reportInterruptAfterWait(interruptMode); }
-
Node addConditionWaiter()
将节点加入到条件队列,同时也会对条件队列做出调整,如果它发现当前节点的前置条件队列节点状态不是condition(可能是取消状态,又可能是0),它就会调用
unlinkCancelledWaiters()
方法,删除所有非condition的节点。思考一下:条件队列中有可能出现什么状态的节点?
- 第一种最常见的,CONDITION的节点,这种节点是正常情况会出现的节点,节点进入条件队列时就是设置为CONDITION。
- 第二种是取消状态,只有节点在下面完全释放锁方法
fullyRelease(node)
中解锁失败,就会把节点设置成取消状态。 - 第三种是初始状态0,这个假如我们调用了
sigal()
方法就会把条件队列头节点设置成0,然后把该节点转移到同步队列,但假如在转移过程中发生了线程中断,那节点虽然被设置成了0,也被加入了阻塞队列中,但它还没从条件队列中删除。
private Node addConditionWaiter() { // 获取条件队列尾节点 Node t = lastWaiter; // 队列不为空,且前置节点状态不是condition if (t != null && t.waitStatus != Node.CONDITION) { // 删除所有非condition的节点 unlinkCancelledWaiters(); t = lastWaiter; } // 封装节点,状态是condition Node node = new Node(Thread.currentThread(), Node.CONDITION); // t此时是尾节点,这里表示队列为空 if (t == null) // 当前节点设置为头节点 firstWaiter = node; // 队列不为空 else // 加入节点 t.nextWaiter = node; lastWaiter = node; // 返回该节点 return node; }
-
unlinkCancelledWaiters()
删除条件队列中所有非CONDITION的节点,这个操作是纯链表操作,没啥好分析的,源码随意看看就好。要了解的是它被调用的时机:
- 当节点进入条件队列的时候,如果队列不为空,且尾节点状态是取消状态,就会调用一次这个方法
- 当节点被sigal唤醒后,从条件队列转移到同步队列期间发生中断,会调用该该方法清除,还来不及从条件队列删除的节点。
private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
-
fullyRelease(node)
完全释放锁,并返回释放的锁的数量。也要注意一些细节,当解锁失败后,节点会被修改为取消状态,且会直接报错。下面也要思考下两个问题:
- 为什么要释放锁呢?因为要调用
await()
方法的前提是,该线程获得了锁,现在要挂起它,所以他的锁肯定要释放啦 - 为什么要记录释放的锁的数量呢?这里是与重入锁有关的,假如当前是重入锁,也就是说线程拥有多把锁(state > 1),我们这里把锁都给释放了,等以后唤醒线程以后是不是还得把锁还给他,那还给他又不能还少了对吧,所以要记录起释放的数量。
final int fullyRelease(Node node) { // 完全释放锁失败标志,默认为失败 boolean failed = true; try { // 保存持有锁的数量 int savedState = getState(); // 释放n把锁 if (release(savedState)) { // 解锁成功,标志改为false failed = false; // 返回解锁的数量 return savedState; } else { // 解锁失败,直接报错 throw new IllegalMonitorStateException(); } } finally { // 解锁失败会进入这里面,把当前节点改成取消状态,等待被回收 if (failed) node.waitStatus = Node.CANCELLED; } }
- 为什么要释放锁呢?因为要调用
-
isOnSyncQueue(node)
判断节点是否位于同步队列中,用于判断线程是否被唤醒了。这个方法虽然代码看起来挺少,但是挺复杂的,它对于多种情况都做了处理。
大家来思考两个问题:
-
在源码中使用
node.prev == null
来判断节点不位于同步队列中,那第二个判断中为什么不使用node.prev != null
来判断节点位于同步队列中。答案就是,enq方法可能尚未完成,所以这种情况可能就是节点只是有了前置节点,但它还没加入条件队列,把自己设置成队列尾部的CAS操作可能还没完成,或者失败了。 -
一般来说,在第一个if语句中就已经结束了,那什么时候能跳过第一个if语句的情况呢?就是在sigal操作之后被中断(在节点转移的过程中被中断),才有可能发生。
// 返回的结果表示节点是否存在于同步队列中 final boolean isOnSyncQueue(Node node) { // 节点状态为condition表示一定存在于条件队列中,因为使用sigal转移到同步队列的时候,节点状态会被改为0 // 前置节点为null的时候,也可以表示节点不存在于同步队列,因为假如加入了同步队列它无论如何都会有前置节点 if (node.waitStatus == Node.CONDITION || node.prev == null) return false; // 节点的后继节点不为null,说明节点肯定在队列中,返回true,prev和next都是针对同步队列的节点 if (node.next != null) return true; // 这个方法作用是从同步队列中,从后往前找看是否有该节点存在,算是一个兜底方法 return findNodeFromTail(node); } // 从同步队列往后向前找,判断当前节点是否存在于同步队列中 private boolean findNodeFromTail(Node node) { Node t = tail; for (;;) { if (t == node) return true; if (t == null) return false; t = t.prev; } }
-
-
void signal()
唤醒方法,规定了整个唤醒的流程,比起阻塞方法简单了很多。整个唤醒流程其实只做了一件事,就是转移节点,从条件队列到同步队列。不要被名字误导,以为这里就是唤醒了线程,其实并没有。
使用
await()
方法说到底,其实就是把原来获取到锁的线程的锁给卸了,然后把它丢到条件队列里面,现在使用sigal()
方法,其实就是把它从条件队列里面放出来了而已,被卸掉的锁还得它自己去拿。所以唤醒线程这个操作,还是得等它去同步队列中排队排到了才会被唤醒(Locksupport.unpark)。具体看下面源码分析:
// 唤醒等待了最久的线程 // 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列 public final void signal() { // 判断是否获得了锁,没有直接报错,不准唤醒 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // 获取条件队列首节点 Node first = firstWaiter; // 条件队列不为空 if (first != null) // 唤醒头节点 doSignal(first); } // 从条件队列队头往后遍历,找出第一个需要转移的 node // 因为前面我们说过,有些线程会取消排队,但是可能还在队列中 private void doSignal(Node first) { do { // 将 firstWaiter 指向 first 节点后面的第一个,因为 first 节点马上要离开了 // 如果将 first 移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; // 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉 first.nextWaiter = null; // 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推 } while (!transferForSignal(first) && (first = firstWaiter) != null); } // 将节点从条件队列转移到阻塞队列 // true 代表成功转移 // false 代表在 signal 之前,节点已经取消了 final boolean transferForSignal(Node node) { // CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消, // 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点 // 否则,将 waitStatus 置为 0 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // enq(node): 自旋进入阻塞队列的队尾 // 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点 Node p = enq(node); // ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒之后会怎么样,后面再解释 // 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用上面独占模式说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1) int ws = p.waitStatus; // 这个判断和操作是为了提高性能,如果没有这步操作其实也不会出错,这里不再具体分析了 // 具体可以看这篇博客:https://blog.csdn.net/dataiyangu/article/details/105029337 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
-
checkInterruptWhileWaiting(node)
这个方法是检查线程是否被中断,返回值之前已经有说了,假如节点发生了中断还会把节点转移到阻塞队列中。这个方法是线程被唤醒后才会执行的,现在我们可以先来看一下什么时候会唤醒线程:
- 被SIGAl后,节点转移到同步队列,然后节点在同步队列中获得了锁,会被唤醒,这个是最正常的情况。
- 被SIGAL后,加入同步队列,发现前置节点是取消状态,这个时候也会被唤醒,这个上面已经说了,为了提高性能
- 被中断,被中断后也会醒来。
关于这个方法,我们要知道的有两点:
- 这个方法会检查中断情况,如果发生了中断,会判断是在sigal前或者sigal后中断的,如果是在sigal前中断的还要对节点做出一些处理。为什么呢?因为sigal前发生了中断的话该节点就不会执行sigal操作了,sigal操作做了什么呢?修改节点状态,把该节点转移到同步队列。所以这里就要完成sigal操作的处理,所以对于sigal前中断的节点,会修改节点状态为0,并加入到同步队列,但要注意一点!!!不会从条件队列中删除,也就是说该节点同时存在于条件队列和同步队列,后面会对他进行删除。
- 发生了中断,都会从条件队列转移到同步队列,这里描绘了一个场景,本来有个线程,它是排在条件队列的后面的,但是因为它被中断了,那么它会被唤醒,然后它发现自己不是被 signal 的那个,但是它会自己主动去进入到阻塞队列。
while (!isOnSyncQueue(node)) { // 挂起线程 LockSupport.park(this); // 获得中断标记,checkInterruptWhileWaiting这个方法不仅会检查线程的中断,如果发生了中断还会把节点转移到阻塞队列里 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } private int checkInterruptWhileWaiting(Node node) { // 判断线程是否中断,不是则返回0,是则进入transferAfterCancelledWait方法 return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; } // 该方法判断线程是在sigal前还是后中断的,同时也会把中断的节点转移到同步队列 // 只有线程处于中断状态,才会调用此方法 // 如果需要的话,将这个已经取消等待的节点转移到阻塞队列 // 返回 true:如果此线程在 signal 之前被取消, final boolean transferAfterCancelledWait(Node node) { // 把节点状态修改成0 // 如果这步 CAS 成功,说明是 signal 方法之前发生的中断,因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0 if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 加入条件队列 // 注意!!!这里并没有从条件队列中删除,所以下面需要调用一下清理方法清除 // 这里我们看到,即使中断了,依然会转移到阻塞队列 enq(node); return true; } // 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0 // signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成 // 当然,这种事情还是比较少的吧:signal 调用之后,没完成转移之前,发生了中断 while (!isOnSyncQueue(node)) Thread.yield(); return false; }
-
reportInterruptAfterWait(interruptMode)
对中断做处理,sigal前抛出异常,sigal后不做处理,再次中断。
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt(); }