AQS 条件模式
条件模式使用示例:
public class Run315 {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
ThreadAA aa = new ThreadAA(service,"aa");
aa.start();
Thread.sleep(3000);
service.signal1();
service.signal2();
}
}
class Service{
private Lock lock = new ReentrantLock();
public Condition condition1 = lock.newCondition();
public Condition condition2 = lock.newCondition();
public void await1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " ---- await1");
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void signal1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " ---- signal1");
condition1.signal();
}finally {
lock.unlock();
}
}
public void await2(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " ---- await2");
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void signal2(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " ---- signal2");
condition2.signal();
}finally {
lock.unlock();
}
}
}
class ThreadAA extends Thread{
private Service service;
private String name;
public ThreadAA(Service service, String name){
this.service = service;
this.name = name;
}
@Override
public void run() {
service.await1();
System.out.println(Thread.currentThread().getName() + " released ---- await1");
service.await2();
System.out.println(Thread.currentThread().getName() + " released ---- await2");
}
}
运行上述代码,会出现一条 signal 未响应的情况,可以分析下原因
匆忙补充的示例,想验证下一个lock,多个 condition 的情况,本文的重点是源码分析,后续会继续从应用中的问题再回顾源码
Condition 接口中的 await/signal 与 object 中的 wait/notify
ConditionObject 与 Condition 之间的关系
ConditionObject 是 AQS 的内部类,实现了 Condition 接口,主要有两个属性 firstWaiter,lastWaiter
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 条件队列头结点
private transient Node firstWaiter;
// 条件队列队尾结点
private transient Node lastWaiter;
// 构造器,延迟初始化,使用时才会初始化条件队列
public ConditionObject() { }
}
1 await()
public final void await() throws InterruptedException {
// 1. 若当前线程在调用 await() 方法前已经被中断,直接抛出 InterruptedException() 异常
if (Thread.interrupted())
throw new InterruptedException();
// 2. 将当前线程封装成 Node 对象添加到 条件队列中
Node node = addConditionWaiter();
// 3. 释放当前线程所占用的锁,保存当前锁的状态
int savedState = fullyRelease(node);
// interruptMode {THROW_IE, REINTERRUPT}
int interruptMode = 0;
// 4. 若当前队列不在同步队列中,说明刚执行await()方法,还未被 signal/signalAll 方法唤醒
while (!isOnSyncQueue(node)) {
// 4.1 阻塞线程
LockSupport.park(this);
// 4.2 检查线程被唤醒的原因,若是中断被唤醒,则跳出 while 循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await()
执行流程:
- 若线程在调用
await()
前被中断则直接抛出异常 - 调用
addConditionWaiter()
封装当前线程到等待队列中 - 调用
fullyRelease(node)
释放当前线程所占用的锁 - 将等待队列中的结点添加到同步队列中【中断或者
signal/signalAll
】 - 调用
acquireQueued()
到同步队列中竞争资源 - 竞争到同步资源后,根据
interruptMode
处理中断
1.1 addConditionWaiter()
await() 通过 addConditionWaiter() 将当前线程封装成Node 对象,使之进入条件队列中
private Node addConditionWaiter() {
// 1. 获取条件队列的队尾结点
Node t = lastWaiter;
// 2. 若队尾结点被 cancel,则遍历条件队列
if (t != null && t.waitStatus != Node.CONDITION) {
// 2.1 清除已经取消等待的线程
unlinkCancelledWaiters();
t = lastWaiter;
}
// 3. 将当前线程包装成Node对象放入条件队列
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 4.1 若队尾结点为空,此时队列无节点处,设置node为头结点
if (t == null)
firstWaiter = node;
else
// 4.2 队尾结点非空,设置队尾的nextWaiter 指向 node 结点
t.nextWaiter = node;
// 5. 更新队尾结点为node结点
lastWaiter = node;
// 6. 返回新添加到条件队列的结点
return node;
}
作业:同步队列入队方法 addWaiter() 和 条件队列入队方法 addConditionWaiter() 的对比
处理流程:
-
获取尾结点
同步队列:tail
等待队列:lastWaiter
-
尾结点处理
同步队列:判断尾结点是否为空,若为空则执行enq(node)
先进行同步队列的初始化再将node添加到队尾
等待队列:判断尾结点是否为空及是否取消等待,若取消则调用unlinkCancelledWaiters()
断开与已取消等待结点的连接 -
添加到尾结点
同步队列:尾结点不为空,CAS尝试添加node入尾,若tail == null 或CAS失败,则在enq(node)
中自旋执行CAS入尾操作
等待队列:尾结点为空,则设置firstWaiter = node
,否则将node设置为lastWaiter.nextWaiter
-
更新尾结点并返回新添加的结点
问题:为什么结点添加到等待队列中时不需要 CAS 操作?
//await()
// 2. 将当前线程封装成 Node 对象添加到 条件队列中
Node node = addConditionWaiter();
// 3. 释放当前线程所占用的锁,保存当前锁的状态
int savedState = fullyRelease(node);
线程执行 await()
方法时,还未释放资源,即线程正持有锁,不存在竞争关系
1.1.1 unlinkCancelledWaiters()
若通过 addConditionWaiter() 方法进入条件队列时尾结点可能处于取消状态,新节点不应该连接在已经取消的结点后面,可通过 unlinkCancelledWaiters() 剔除那些已经取消等待的线程
unlinkCancelledWaiters() 从头结点开始遍历整个队列,剔除掉waitStatus != Node.CONDITION 的结点
问题:addConditionWaiter()
添加新结点前都会执行unlinkCancelledWaiters()
检查尾结点是否取消,什么情况下结点会取消呢?是否只有尾结点被取消?
private void unlinkCancelledWaiters() {
// 1. 获取条件队列头结点
Node t = firstWaiter;
Node trail = null;
// 2. 当前结点不为空
while (t != null) {
// 2.1 next 临时变量保存当前结点的下一个结点
Node next = t.nextWaiter;
// 2.2 若 t 状态不是 Node.CONDITION
if (t.waitStatus != Node.CONDITION) {
// 2.2.1 断开 t 与后继结点的连接
t.nextWaiter = null;
// 2.2.2 trail = null 说明上一个结点为头结点且状态不为 Node.CONDITION,则更新头结点为 next
if (trail == null)
firstWaiter = next;
// 2.2.3 trail != null 当前遍历的结点之前有未取消的结点,且当前结点已取消,则将略过当前结点,使前继结点与该结点的后继结点建立的连接
else
trail.nextWaiter = next;
// 遍历到链表的结尾,则将 trail 设置为 尾结点
if (next == null)
lastWaiter = trail;
}
// 2.2 当前结点状态为 Node.CONDITION 则将其保存到 trail 中
else
trail = t;
// 2.3 继续向后遍历链表结点,
t = next;
}
}
作业:unlinkCancelledWaiters() 的处理流程
问题:trail 的作用是什么?新的等待队列的lastWaiter
问题:条件队列中的结点非null吗?同步队列中的结点呢?
思考:node 什么情况下为 null,声明但未赋值?
① 为 null 说明为引用类型
② 声明但未赋值,只能是成员变量或类变量,私有变量需要在使用前赋值
class Person {
private String name;
public Person(){}
}
public class TestPerson{
static Person p1;
Person p2;
public static void main(String[] args) throws ClassNotFoundException {
Person p = new Person();
System.out.println(p1 == null); // true
}
public void test1(){
Person p3;
System.out.println(p2 == null); // true
}
}
③ 思考加入队列的结点的属性
同步队列:Node {prev, next, nextWaiter}
执行 addWaiter 时,Node node = new Node(Thread.currentThread(), mode);
等待队列:Node {firstWaiter, lastWaiter}
执行 addConditionWaiter 时 Node node = new Node(Thread.currentThread(), Node.CONDITION);
分析至此,暂时说明结点加入队列时是不为null
,but 想起来 AQS 中好像有很多地方需要判断 null,需要针对这个点再捋一捋
借助 trail
将等待队列转移到新的等待队列【过滤掉了 node.waitStatus != Node.CONDITION
的结点】
跳出循环的条件 next == null,即遍历到原 lastWaiter.nextWaiter
上面的流程图还是不太好理解,从其中的属性 {firstWaiter, lastWaiter, t, next, trail} 入手分析
- 不会执行上述
unlinkCancelledWaiters()
的情况:lastWaiter == null
说明队列为空,设置firstWaiter = lastWaiter = node
【从addConditionWaiter
开始看】 - 排除情况
1
说明会执行while
,trial
的作用:新队列的lastWaiter
trial == null
:遍历的t
均处于取消状态
trail == t
:遍历的t
未取消,则遍历第一个结点后,trail = firstWaiter
- 有取消的结点的情况
trial == null
:更新firstWaiter = next
trail == t
:已借助trail
形成新的等待队列,将添加到新队列的队尾 - 判断
next
的状态,考虑next == null
时,否则继续进行2-4
trial == null
:firstWaiter = next = null
,lastWaiter = trail= null
,即等待队列中仅有一个结点,该结点还取消了,此时并未建立新的等待队列
trail == t
:此时trail.nextWaiter = next = null
,设置lastWaiter = trail
,并退出循环
上面的流程也说明:等待队列不存在 dummy head
执行为null
的情况为:
① 队列为空:firstWaiter = lastWaiter = null
② 队列不为空:lastWaiter.nextWaiter = null
1.2 fullyRelease()
await() 方法通过 fullyRelease() 释放当前线程持有的同步资源
fullyRelease(node) -> release(arg) ->tryRelease(arg)【由子类实现】
此小节可以部分回答上面,addConditionWaiter()
添加新结点前都会执行unlinkCancelledWaiters()
检查尾结点是否取消?
final int fullyRelease(Node node) {
// 1. 是否成功释放标识
boolean failed = true;
try {
// 2. 获取资源状态
int savedState = getState();
// 3. 释放资源成功
if (release(savedState)) {
// 3.1 设置释放成功标识
failed = false;
// 3.2 返回资源状态
return savedState;
} else {
// 4. 当前线程不是持有锁的线程,则抛出 IllegalMonitorStateException 异常
throw new IllegalMonitorStateException();
}
} finally {
// 5. 释放资源失败,则将结点的状态设置为取消
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
什么情况下node.waitStatus = Node.CANCELLED
?
fullyRelease(node)
执行流程:
- finally 一定会执行,因此若
failed = true
则会执行node.waitStatus = Node.CANCELLED
failed = false
的情况:成功执行release(arg)
failed = true
的情况:执行失败,抛出IllegalMonitorStateException
异常
抛出IllegalMonitorStateException
异常的原因?
1.2.1 release(arg)
public final boolean release(int arg) {
// 1. 子类重写的 tryRelease(arg) 执行成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 1.2 唤醒头节点的后继节点
unparkSuccessor(h);
// 1.3 返回释放成功标识
return true;
}
// 2. tryRelease(arg) 执行失败,返回释放失败标识
return false;
}
1.2.2 tryRelease(arg)
以 ReentrantLock
重写的tryRelease(arg)
为例
protected final boolean tryRelease(int releases) {
// 1. 获取资源状态
int c = getState() - releases;
// 2. 判断当前线程是否是持有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
//2.1 当前线程不是持有锁的线程则抛出 IllegalMonitorStateException 异常
throw new IllegalMonitorStateException();
// 3. 设置释放成功标识
boolean free = false;
// 4. 若state = 0已获取的资源全部释放
if (c == 0) {
// 4.1 返回释放成功
free = true;
// 4.2 清除线程的独占状态
setExclusiveOwnerThread(null);
}
// 5. 更新 state
setState(c);
// 6. 返回释放状态
return free;
}
问题:这里抛出异常后,还会执行下面的 return free 吗?
测试用例:
① testThrow() 中抛出异常
② 函数调用流程:main()->testFullyRelease()->testRelease()->testThrow()
public class TestThrow {
public static void main(String[] args) {
testFullyRelease();
}
public static void testFullyRelease(){
boolean failed = true;
int state = -1;
try{
if(testRelease(state)){
failed = false;
}
}finally{
if(failed){
System.out.println("IllegalMonitorStateException");
}
}
}
public static boolean testRelease(int arg){
if(testThrow(arg)){
return true;
}
return false;
}
public static boolean testThrow(int arg){
if(arg < 0){
throw new IllegalMonitorStateException();
}
boolean free = false;
if(arg != -1){
free = true;
}
return free;
}
}
测试结果:
IllegalMonitorStateException
Exception in thread "main" java.lang.IllegalMonitorStateException
at TEST.TestThrow.testThrow(TestThrow.java:36)
at TEST.TestThrow.testRelease(TestThrow.java:28)
at TEST.TestThrow.testFullyRelease(TestThrow.java:17)
at TEST.TestThrow.main(TestThrow.java:10)
测试结论:
- 抛出异常,程序后面的语句不再执行,即若在 tryRelease(arg) 中抛出异常,则不会执行
return free
- 抛出异常方法的调用者
release(arg)
中没有异常处理程序,会继续向上抛出异常,并且不会执行return free
- 若在
tryRelease(arg)
抛出异常,fullyRelease(node)
中抛出的IllegalMonitorStateException
来自调用的函数tryRelease(arg)
,而非 else 分支 fullyRelease(node)
中else
分支抛出的异常可能是:持有资源的线程为完全释放资源
经过上面的分析后:节点取消的原因可能是:当前线程不是持有资源的线程,或者持有资源的线程未完全释放资源【有待考究】
即目前看到的,节点取消是发生在释放资源的过程中
那么取消的节点会在什么地方呢?同步队列还是等待队列
同步队列中是阻塞线程,线程进入同步队列有两种可能
① 未获取到同步资源,执行 Lock.park()
② 获取到同步资源,调用 await(),执行 signal() 或被中断,执行 enq(node) 加入同步队列
若是持有资源但未完全释放的线程,则可能执行了await()
添加到等待队列中,但执行fullyRelease()
失败,设置 node.waitStatus = Node.CANCELLED
,执行unlinkCancelledWaiters()
从等待队列中移除
而持有资源的线程会从同步队列中移除,因此对于持有资源但未完全释放的线程,可能是继续释放资源,而不在队列中等待或阻塞
若线程不是持有资源的线程,则不会执行 await()【?】,这一部分没有想清除
该疑点还与接下来的问题有关
为什么 执行 await()
的时候不判断是持有当前线程的资源,而在释放资源的时候判断 ?
protected final boolean tryRelease(int releases) {
// 1. 获取资源状态
int c = getState() - releases;
// 2. 判断当前线程是否是持有锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
这里,因为 tryRelease(arg)
调用的是子类,所以在 await()
中相当于没有直接判断释放资源的线程,可能需要从子类中找答案
注意到 getExclusiveOwnerThread()
是 AbstractOwnableSynchronizer
中的方法,AQS是其子类,但并没有重写该方法
1.3 isOnSyncQueue()
挂起线程之前,先用 isOnSyncQueue() 保证线程不在同步队列中
final boolean isOnSyncQueue(Node node) {
// 1. node 状态为 Node.CONDITION 或 前驱节点为空,说明在条件队列中或者是还未加入同步队列中的结点
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 2. node 的 next 引用非空,则肯定在同步队列中
if (node.next != null) // If has successor, it must be on queue
return true;
// 3. 将线程放入同步队列的操作 addWaiter() 添加node 到队尾,先将node.prev 指向 old node 再进行 cas 操作,若 cas 操作失败,则node.prev = old tail, node.next == null,因此需要进一步判断
return findNodeFromTail(node);
}
如何判断节点是否在同步队列中?
节点可能的状态:不在队列中在运行态,在等待队列,在同步队列
等待队列:node.waitStatus == Node.CONDITION
,由于等待队列的Node没有使用 prev,next
,即若节点在等待队列中,node.prev == null
同步队列:节点的状态可能为:{ Node.SIGNAL, Node.PROPAGATE, 0}
,
节点可能正在队列中,node.next != null
或正在加入队列,执行addWaiter
时,先建立与尾节点的连接,在执行CAS入尾,此时 node.prev != null
1.3.1 findNodeFromTail()
执行 findNodeFromTail() 是进一步判断是否是:结点加入同步队列,node.prev = old tail 但是 CAS 失败,出现 node.prev != null 但 node.next == null 的情况
private boolean findNodeFromTail(Node node) {
// 1. 获取队尾结点 tail,仅当同步队列 tail 才非空
Node t = tail;
for (;;) {
// 1. 若 tail = node 则是同步队列
if (t == node)
return true;
// 2. 若 tail = null 则是条件队列
if (t == null)
return false;
t = t.prev;
}
}
从队尾向前遍历,t != null 可以判断不是条件队列,但执行该步时,可能有线程又加入了队尾,
因此向前搜索待查找结点,此时该节点的CAS必然是已经成功的
2 signalAll()
ConditionObject 重写的await() 方法执行isOnSyncQueue(node)
判断 node 不在同步队列中,node 封装的线程会被 LockSupport.park(this)
挂起,可通过 signal/signalAll() 方法或中断唤醒被挂起的线程,因此本段代码若要继续向下分析,需要先将线程从条件队列中唤醒
while (!isOnSyncQueue(node)) {
// 4.1 阻塞线程
LockSupport.park(this);
// 4.2 检查线程被唤醒的原因,若是中断被唤醒,则跳出 while 循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
调用 signalAll() 方法的线程必须是已经持有资源的线程
// 将等待条件队列中的线程移到等待锁的队列中
public final void signalAll() {
// 1. 若当前线程未持有资源(lock)则抛出 IllegalMonitorStateException 异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 2. 获取条件队列的头节点
Node first = firstWaiter;
// 3. 若条件队列头节点非空,则唤醒队列的头结点
if (first != null)
doSignalAll(first);
}
检查当前线程是否持有同步资源 isHeldExclusively
由继承的子类实现
protected final Thread getExclusiveOwnerThread() {
//1. exclusiveOwnerThread 保存了当前持有资源的线程
return exclusiveOwnerThread;
}
protected final boolean isHeldExclusively() {
// 2. 获取当前线程并判断是否是持有资源的线程
return getExclusiveOwnerThread() == Thread.currentThread();
}
2.1 doSignalAll()
若当前线程持有同步资源并且队列的头节点非空,则调用 doSignalAll() 方法唤醒所有等待的结点
private void doSignalAll(Node first) {
// 1. 将队列的头尾结点置空
lastWaiter = firstWaiter = null;
do {
// 2.从条件队列的头节点开始依次将每个结点添加到同步队列中
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
注意这里:使用 signal
方式唤醒的时候,会将 node 移除等待队列first.nextWaiter = null;
,而若通过中断的方式,直接执行enq(node)
不会移除,这也是判断线程在等待队列中阻塞时是通过中断唤醒还是通过 signal
唤醒的标志之一
2.1.1 transferForSignal(node)
transferForSignal(node) 将原条件队列中的结点添加到同步队列末尾
final boolean transferForSignal(Node node) {
//1. 若 node.waiStatus != Node.CONDITION,说明结点已取消,返回添加失败
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 2. CAS 置 node.waitStatus = 0 成功则利用 enq() 将该结点添加至同步队列末尾
Node p = enq(node);
// 3. node 添加至同步队尾成功,返回其前驱结点 p, 并获取 p.waitStatus
int ws = p.waitStatus;
// 4. 若前驱结点已取消或者 唤醒前驱结点失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
上述执行compareAndSetWaitStatus(node, Node.CONDITION, 0)
失败,直接返回 false 的情况分析
执行 signalAll() 的线程是唤醒线程,唤醒的对象是等待队列中的节点,而等待队列中的节点有两种状态
{Node.CONDITION, Node.CANCELLED}
,因此节点若取消则不会在队列中,不用添加到同步队列【关于 Node.CANCELLED 理解还有点问题】
问题:CAS设置 waitStatus = Node.SIGNAL 时,为什么会直接唤醒线程?
思考:有无可能 有别的线程在修改 node.prev 的状态?node.prev = head ? node.prev.waitStatus = Node.PROPAGATE?
若不是第一种情况,则 node.prev 还未获取同步资源,所以不会唤醒 node,但是对于后面的两种情况,是可以唤醒的
作业:总结 signalAll() 唤醒条件队列中结点的流程
3 signal()
signal() 只会唤醒条件队列中的一个结点,即唤醒条件队列中第一个没有取消的结点
public final void signal() {
// 1. 检查当前线程[调用该方法的线程]是否持有资源
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 2. 若条件队列头结点非空,则调用 doSignal(node) 唤醒结点
if (first != null)
doSignal(first);
}
3.1 doSignal(node)
doSignal(node) 唤醒第一个没有取消等待的结点
private void doSignal(Node first) {
do {
// 1. 若头结点为空则条件队列中无结点,将条件队列的头尾结点置空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 2. 若结点被成功添加到同步队列中则跳出while循环
} while (!transferForSignal(first) &&
// 2.1 会执行 && 后面的说明该结点已取消则继续向后遍历添加未取消的结点,直到队列为空
(first = firstWaiter) != null);
}
signal() 只会唤醒队列中的一个节点,若唤醒成功则transferForSignal(first)
返回true,退出while循环
若节点取消,则transferForSignal(first)
返回 false,会唤醒未取消的节点
作业:signal() 和 signalAll() 的区别
4 等待线程从LockSupport.park(this)
状态退出
LockSupport.park(this) 会阻塞当前线程,即线程会暂停在该处,线程继续执行有两种原因:
① 由其它线程通过 signal/signalAll() 唤醒
② 线程被中断,解除阻塞状态
public final void await() throws InterruptedException {
// 1. 若当前线程在调用 await() 方法前已经被中断,直接抛出 InterruptedException() 异常
if (Thread.interrupted())
throw new InterruptedException();
// 2. 将当前线程封装成 Node 对象添加到 条件队列中
Node node = addConditionWaiter();
// 3. 释放当前线程所占用的锁,保存当前锁的状态
int savedState = fullyRelease(node);
//
int interruptMode = 0;
// 4. 若当前队列不在同步队列中,说明刚执行await()方法,还未被 signal/signalAll 方法唤醒
while (!isOnSyncQueue(node)) {
// 4.1 阻塞线程
LockSupport.park(this);
// 4.2 检查线程被唤醒的原因,若是中断被唤醒,则跳出 while 循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
执行await()
方法的线程执行LockSupport.park(this)
会进入阻塞状态,其它线程执行 signal/signalAlll 方法时会被唤醒,继续执行后面的流程
后续流程包括:
interruptMode
获取中断模式,判断线程在等待过程中是否发生中断acquireQueued(node, state)
在同步队列中自旋-阻塞-自旋获取同步资源,线程获取资源成功后,该方法返回在同步队列中获取资源是否中断return interrupted
- 若
node.nextWaiter != null
,则执行unlinkCancelledWaiters
,对应中断唤醒的情况,直接执行的 ``enq(node)```并未将节点移除等待队列 - 若发生中断,则报告中断状态
问题:验证退出while
循环时,节点已从等待队列转移到同步队列
4.1 interruptMode
await() 方法中使用 interruptMode 记录中断事件
// REINTERRUPT 退出 await() 方法时只需要再次中断线程,对应中断发生在 signal/signalAll 方法被调后
private static final int REINTERRUPT = 1;
// THROW_IE 退出 await() 方法时需要抛出 InterruptExceptin, 对应中断发生在 signal/signalAll 方法被执行之前
private static final int THROW_IE = -1;
interruptMode = 0 代表这个过程中一直没有发生中断
4.2 checkInterruptWhileWaiting()
线程被唤醒后,首先使用 checkInterruptWhileWaiting() 检测中断
private int checkInterruptWhileWaiting(Node node) {
// 1. 通过 Thred.interrupted() 判断线程是否中断,若未中断返回 0
// 2. 若中断,则通过 transferAfterCancelledWait(node) 判断中断类型
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
4.2.1 transferAfterCancelledWait(node)
将node结点移到同步队列中
返回两种中断对应的结果{THROW_IE , REINTERRUPT }
线程中断发生在被signal 唤醒前:transferAfterCancelledWait(node) = true
线程中断发生在被signal 唤醒后:transferAfterCancelledWait(node) = false
final boolean transferAfterCancelledWait(Node node) {
// 1. node.waitStatus = Node.CONDITION 说明该结点改为被 signal/signalAll 唤醒过
// 若线程被 signal/signalAll 唤醒,则 transferForSignal 中会首先设置 node.waitStatus = 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 1.1 中断发生在结点被 signal 唤醒前,则调用 enq(node) 将该结点添加到同步队列
enq(node);
// 1.2 返回true, 对应 interruptMode = THROW_IE 的情况
return true;
}
// 2. CAS 失败说明线程已经被 signal/signalAll 唤醒, 等待线程进入同步
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
问题:此处 yield() 的作用是?
① 执行 yield()
说明CAS设置节点node.waitStatus = 0 失败,线程是被 signal/signalAll
唤醒的
node 被 signal 唤醒时,会CAS更新 node.waitStatus = 0
final boolean transferForSignal(Node node) {
//1. 若 node.waiStatus != Node.CONDITION,说明结点已取消,返回添加失败
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
② node.waitStatus == Node.CONDITION
线程的 waitStatus
未更新,说明线程是在阻塞时被中断唤醒
③ 被中断唤醒会在transferAfterCancelledWait(node)
中执行CAS设置 node.waitStatus = 0,CAS成功后会执行enq(node)
将节点移至同步队列
此处也回答了上述问题,退出 while 循环时,节点一定进入了同步队列,因为通过 signal/signalAll 或者中断的方式都会将节点添加到同步队列
④ 若不是被中断唤醒,假设执行 signalAll 的线程为A,被唤醒的线程为B,则在A执行transferForSignal
将唤醒的封装B的节点加入同步队列的同时,线程B若被中断则开始执行 if 条件中的checkInterruptWhileWaiting
,可能会出现B还未加入同步队列中的情况。此时,需要使已被唤醒的线程B自旋,等待唤醒线程A将其添加到同步队列
流程图
上图中加入中断,原因是:若执行 yield()
,说明线程必然被 signal
唤醒,但出现判断是否中断时出现还未入尾的情况
final boolean transferForSignal(Node node) {
....
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
}
由于唤醒的流程是:先执行enq(node)
,再执行LockSupport.unpark(node.thread)
若无中断,不会出现,线程被唤醒但是还未入尾的情况
5 中断处理
等待线程从阻塞状态解除后,被添加到同步队列,通过checkInterruptWhileWaiting
将中断模式保存在interruptMode
中
接下来的执行流程是:
- 在同步队列
acquireQueued
获取到资源 - 判断中断模式,执行
reportInterruptAfterWait
报告中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
考虑可能发生中断的情况:
① 中断发生在 signal
之前:在 LockSupport.park(thread)
阶段中断,通过checkInterruptWhileWaiting
添加到同步队列
② 中断发生在 signal
之后:通过 transferForSignal(node)
添加到同步队列,未执行获取同步资源前中断,对应上述分析 yield()
的情况
③ 中断发生在同步队列中获取资源时:acquireQueued(node, state)
返回中断状态
问题:会否被中断两次?即①+②,或① +③的情况
首先回顾下 checkInterruptWhileWaiting
中检测到的中断的处理
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
transferAfterCancelledWait(node)
= true :被中断唤醒,THROW_IE
transferAfterCancelledWait(node)
= false:signal后被中断,REINTERRUPT
【会先执行添加到同步队列并获取资源,并不处理中断】
checkInterruptWhileWaiting
不止保留了阻塞后唤醒的中断状态检查,还对中断标志位做了处理
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
isInterrupted(true)
参数为true
会清除中断标志位,也即在①或②中发生中断后,在③中也可能发生中断
下面逐条分析上述三条 if 语句执行的对应情况:
5.1 interruptMode = REINTERRUPT
对应上述③ + ② 或者 ③
③ 阻塞唤醒阶段未中断,在同步队列中获取同步资源时中断
② + ③ 在被signal唤醒后发生中断,同时在获取同步资源时又发生了中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
5.2 interruptMode = THROW_IE
对应 ① 或 ① + ③
此时判断是否在获取同步资源时中断没有意义,后续报告中断主要是根据interruptMode
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
为什么对应 ① 呢?
可以回顾上述分析 signal
的流程,通过signal/signalAll
唤醒,添加到同步队列前会设置node.nextWaiter != null
而通过中断解除阻塞状态时,节点也会从等待队列转移到同步队列,但是仅执行了enq(node)
并没有从等待队列中移除
此时节点的状态是:node.waitStatus = 0
,需要执行unlinkCancelledWaiters()
从同步队列中移除
5.3 reportInterruptAfterWait(interruptMode)
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
//1. interruptMode = THROW_IE 则抛出异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 2. interruptMode = REINTERRUPT 则再次中断一次
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
interruptMode = THROW_IE
则抛出异常
interruptMode = REINTERRUPT
则再次中断一次
5.4 interruptMode = 0
这种情况下对应①,②,③都没有发生,因此不会执行reportInterruptAfterWait(interruptMode)
=_= ,有一种上瘾且吐血的感觉,继续加油,持续改进