文章目录
1、前言
一个新设计的出现,总是为了替换现有的略有不足的设计。而Condition接口的出现,是为了代替监视器锁的wait/notify机制,提供更强大的功能。
我们将Object
自带的wait/notify
方法与Condition
接口提供的await/signal
进行一个对比。
Object方法 | Condition方法 | 区别 |
---|---|---|
void wait() | void await() | |
void wait(long timeout) | long awaitNanos(long nanosTimeout) | 时间单位:前者毫秒ms,后者纳秒ns 返回值 |
void wait(long timeout, int nanos) | boolean await(long time, TimeUnit unit) | 时间单位:前者只能纳秒,后者什么单位都可以 返回值 |
void notify() | void signal() | |
void notifyAll() | void signalAll() | |
- | void awaitUninterruptibly() | Condition独有 |
- | boolean awaitUntil(Date deadline) | Condition独有 |
-
调用wait()的线程必须已经处于同步代码块中,换言之,调用wait()的线程已经获得了监视器锁;调用await()的线程则必须是已经获得了lock锁。
-
执行wait()时,当前线程会释放已获得的监视器锁,进入到该监视器的等待队列中;执行await()时,当前线程会释放已获得的lock锁,然后进入到该Condition的条件队列中。
-
退出wait()时,当前线程又重新获得了监视器锁;退出await()时,当前线程又重新获得了lock锁。
-
调用监视器的notify,会唤醒等待在该监视器上的线程,这个线程此后才重新开始锁竞争,竞争成功后,会从wait方法处恢复执行;调用Condition的signal,会唤醒等待在该Condition上的线程,这个线程此后才重新开始锁竞争,竞争成功后,会从await方法处恢复执行。
2、同步队列 和 条件队列
对于每个Condition对象来说,都对应到一个条件队列condition queue。而对于每个Lock对象来说,都对应到一个同步队列sync queue。
2.1sync queue
独占锁的获取过程中,我们提到了,每个线程在lock()尝试获取锁失败后,都会被包装成一个node放到sync queue中去。
sync queue是一个双向链表,它使用prev和next作为链接。在这个队列中,我们几乎不关心节点的nextWaiter成员,最多会在共享锁模式下,用来标识节点是否为共享锁节点。队头是一个dummy node即Thread成员为null,第一个等待线程永远只能是head的后继。
2.2、condition queue
每一个Condition对象都对应到一个条件队列condition queue
,而每个线程在执行await()
后,都会被包装成一个node放到condition queue
中去。
condition queue是一个单向链表,它使用nextWaiter作为链接。这个队列中,不存在dummy node,每个节点都代表一个线程。这个队列的节点的状态,我们只关心状态是否为CONDITION,如果是CONDITION的,说明线程还等待在这个Condition对象上;如果不是CONDITION的,说明这个节点已经前往sync queue了。
2.3、condition queue与sync queue关系
本来队列是没有任何关系的,但是如果调用了signal,则会将condition队列上的节点转移到sync上。
上图简单体现了节点从从condition queue
转移到sync queue
上去的过程。即使是调用signalAll
时,节点也是一个一个转移过去的,因为每个节点都需要重新建立sync queue
的链接(enq方法)。
-
如果一个节点刚入队sync queue,说明这个节点的代表线程没有获得锁(尝试获得锁失败了)。
-
如果一个节点刚出队sync queue(指该节点的代表线程不在同步队列中的任何节点上,因为它已经跑到了AQS的exclusiveOwnerThread成员上去了),说明这个节点的代表线程刚获得了锁(尝试获得锁成功了)。
-
如果一个节点刚入队condition queue,说明这个节点的代表线程此时是有锁了,但即将释放。
-
如果一个节点刚出队condition queue,因为前往的是sync queue,说明这个节点的代表线程此时是没有获得锁的。
在condition队列中的获取锁与释放锁,并没有真正的执行到我们的代码中,只有通过acquireQueued获取到锁才会真正的执行到我们的代码中即lock或await调用处。
3、CondtionObject
对于ReentrantLock来说,我们使用newCondition
方法来获得Condition接口的实现,而ConditionObject
就是一个实现了Condition接口的类。
//ReentrantLock.java
public class ReentrantLock implements Lock, java.io.Serializable {
public Condition newCondition() {
return sync.newCondition();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final ConditionObject newCondition() {
return new ConditionObject();
}
}
}
而ConditionObject又是AQS的一个成员内部类,这意味着不管生成了多少个ConditionObject,它们都持有同一个AQS对象的引用,这和“一个Lock可以对应到多个Condition”相吻合。这也意味着:对于同一个AQS来说,只存在一个同步队列sync queue,但可以存在多个条件队列condition queue。
成员内部类有一个好处,不管哪个ConditionObject对象都可以调到同一个外部类AQS对象的方法上去。比如acquireQueued方法,这样,不管node在哪个condition queue上,最终它们离开后将要前往的地方总是同一个sync queue。
public abstract class AbstractQueuedSynchronizer{
private transient volatile Node head;
private transient volatile Node tail;
public class ConditionObject implements Condition {
private transient Node firstWaiter;
private transient Node lastWaiter;
}
}
- firstWaiter和lastWaiter分别代表条件队列的队头和队尾。
- 注意,firstWaiter和lastWaiter都不再需要加volatile来保证可见性了。这是因为源码作者是考虑,使用者肯定是以获得锁的前提下来调用await() / signal()这些方法的,既然有了这个前提,那么对firstWaiter的读写肯定是无竞争的,既然没有竞争也就不需要 CAS+volatile 来实现一个乐观锁了。
但是这是最理想的状态,若我们没有获取到锁直接调用await(),同样会抛出异常,但这种异常是因为用户使用不规范造成的。
3.1、await方法解析
public final void await() throws InterruptedException {
1、调用await之前就已经中断线程,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
2、添加到Condition队列中,注意这时线程还是持有锁的
Node node = addConditionWaiter();
3、释放锁,不管当前线程重入锁多少次,都要释放干净,这时线程已经不持有锁了
int savedState = fullyRelease(node);
4、中断状态 0 为中断 1:在合适的地方中断过 -1:在不合适的地方中断过,抛出异常
int interruptMode = 0;
5、是否还在sync 队列中,不是则会park
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
5.1、检查是否中断过,若中断过直接退出循环,就算不在这儿退出循环,也会被正常siganl唤醒执行enq方法加入到sync队列中,同样会退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
6、执行acquireQueued会重新进入sync队列,若执行完acquireQueued说明该线程又重新持有锁了
判断interruptMode是否等于THROW_IE
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
7、node.nextWaiter!=null 只有中断唤醒线程才会成立,这时waitStatus为0,
但是还在Condintion队列中,来一次大清洗,清除不是Condition的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
8、interruptMode根据状态判断是否抛出异常或自我中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
注释介绍了大概的过程,但首先要明确会有哪些线程在执行:
- 执行await的当前线程。这个线程是最开始调用await的线程,也是执行await所有调用链的线程,它被包装进局部变量node中。(后面会以node线程来称呼它)
- 执行signal的线程。这个线程会改变await当前线程的node的状态state,使得await当前线程的node前往同步队列,并在一定条件在唤醒await当前线程。
- 中断await当前线程的线程。你就当这个线程只是用来唤醒await当前线程,并改变其中断状态。只不过await当前线程它自己被唤醒后,也会做和上一条同样的事情:“使得await当前线程的node前往同步队列”。
- 执行unlock的线程。如果await当前线程的node已经是同步队列的head后继,那么获得独占锁的线程在释放锁时,就会唤醒 await当前线程。
理解了这几个线程的存在,对于本文的理解有很大帮助。从用户角度来说,执行await \ signal \ unlock
的前提都是线程必须已经获得了锁。
3.2、addConditionWaiter
private Node addConditionWaiter() {
Node t = lastWaiter;//获得队尾
// 同步队列中的节点只是CONDITION的,就认为是以后将离开条件队列的节点。
// 将调用unlinkCancelledWaiters来一次大清理,并重新获得队尾
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 包装当前线程为node,状态初始为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)// 如果当前队尾为null,那么整个条件队列都没初始化呢
firstWaiter = node; //把新建node作为队头
else // 如果队列有至少一个节点
t.nextWaiter = node; // 把新建node接在队尾后面
lastWaiter = node; //让node成为新队尾
return node;
}
上面函数描述了新建node加入到条件队列中的过程,我们和独占锁获取过程中新建node加入同步队列进行对比:
- 同步队列sync queue的新建node,它的初始状态为0。而条件队列condition queue的新建node的初始状态为CONDITION。
- sync queue如果拥有队头,队头肯定会是一个dummy node(即线程成员为null)。condition queue则不会有一个dummy node,每个节点的线程成员都不为null。
- sync queue是一个双向链表,需要维持前驱和后继都正确。condition queue只是一个单链表,只需要维持后继即可。
- 这里先提前说下,中断await当前线程的线程(这里特指中断操作在signal之前)和执行signal的线程都会使得条件队列上的node的状态从CONDITION变成0。
3.3、unlinkCancelledWaiters
private void unlinkCancelledWaiters() {
Node t = firstWaiter; //获得队头
Node trail = null; //trail用来保存遍历过程中,最近一次发现的状态为CONDITION的节点
while (t != null) { //只要循环变量不为null,循环继续
Node next = t.nextWaiter; //得到循环变量的后继,循环结束前使用
// 如果循环变量不为CONDITION
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null; //首先使得t与后继断开链接
// 如果直到当前循环都还没有发现一个CONDITION的节点
if (trail == null)
firstWaiter = next;//那么将循环变量的后继,作为新队头
// 如果当前循环之前已经发现了一个CONDITION的节点
else
trail.nextWaiter = next;//那么将trail与next相连,相当于跳过了循环变量t
// 如果已经遍历到队尾,需要将trail作为队尾,因为trail才是队列中最后一个为CONDITION的节点
if (next == null)
lastWaiter = trail;
}
// 如果循环变量为CONDITION,则更新trail
else
trail = t;
t = next; //循环结束前,用next更新循环变量
}
}
主要是一些单链表的操作,trail
变量很重要,它用来保存遍历过程中,最近一次发现的状态为CONDITION的节点。
3.4、fullyRelease
在调用fullyRelease
之前,当前线程已经被包装成node放到条件队列中去了。注意,在这个函数以后,我们再也不会对firstWaiter
和lastWaiter
轻举妄动了,因为await()
以后的执行过程中,当前线程很长一段时间内是没有持有锁的。
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();//这里会获得重入锁的总次数
if (release(savedState)) {
failed = false;
return savedState;// 返回重入锁的总次数
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
该方法比较简单,就是无论重入几次,一次性释放清楚,并返回释放了几次,在后续进入sync队列的时候会在设置上重入锁state的值。
3.5、isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
1、第一个条件成了直接返回false,不在sync队列
2、false||true,因当前线程还没获取到锁,必然在sync队列中,
但prev为null则说明条件不成立,直接返回false
3、false||false。无法判断是否在sync队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
4、node.next!=null,因为prev不为null,且next不为null,必然在sync队列
if (node.next != null) // If has successor, it must be on queue
return true;
5、执行到这儿,node不为CONDITION,prev不为null,但next为null
这时要么node是sync队尾;要么是通过cas对tail执行失败,从队尾判断若与node相等则返回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;
}
}
关于状态不为CONDITION,前面有说过,有两种线程可以使得条件队列上的node的状态从CONDITION变成0(中断线程或signal线程)。但现在可以排除中断await当前线程的线程这种情况(之后具体介绍这个流程),因为如果发生了中断,await的while循环直接就会break出循环了(if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;),也就不会执行到isOnSyncQueue函数了。
简单的说,isOnSyncQueue判断节点已经入队同步队列的标准,必须是node已经成为队尾(包括当前是队尾,或者曾经是队尾)。
3.6、await()第一次调用park之后
这时会阻塞在这儿,需要其他线程signal才会继续执行。
此时node的状态为:
- 在数据结构上,node在条件队列上(
addConditionWaiter
)。 - 在执行过程上,node线程当前阻塞在
LockSupport.park(this)
这里。
3.7、signalAll
public final void signalAll() {
1、判断是否持有锁的线程调用signal的
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
2、获取第一个线程
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
1、直接设置为null(注意signal与中断线程退出park的区别)
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
2、设置nextWaiter为null(注意signal与中断线程退出park的区别)
first.nextWaiter = null;
3、转移node,从condition队列转移到sync队列中(调用enq方法)
transferForSignal(first);
4、循环遍历所有的节点
first = next;
} while (first != null);
}
上来就把代表条件队列的队头队尾成员置null,之后别人就无法通过队头队尾找到队列中的节点了,只有当前线程能通过局部变量first来找到队列节点了。
而接下来不断遍历,直到已经遍历到队尾(first != null)。每次遍历中,将当前遍历节点 与 剩下的条件队列链 断开,然后对当前遍历节点执行transferForSignal。
final boolean transferForSignal(Node node) {
1、设置node.waitStatus状态为0.若失败说明有其他线程已经调用了,无论失败与否,都会变为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
2、进入sync队列,放到队尾,剩下的交给独占锁的获取与释放
注意返回的p是node加入之前的队尾,也就是sync队列倒数第二个节点
Node p = enq(node);
int ws = p.waitStatus;
3、p取消了线程或设置SIGNAL失败,则唤醒node,这时会从whil循环中park继续执行,最终执行到acquireQueued
把p的状态修改为SIGNAL,这儿的作用其实就是要把node的前驱设置为SIGNAL
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
- 如果CAS设置node状态从CONDITION变成0失败了,说明node代表线程因中断而已经执行了中断流程中的compareAndSetWaitStatus(node, Node.CONDITION, 0)。这也间接说明了signal流程和中断流程都是以 成功设置node状态 作为标准,哪个流程成功了,哪个流程就把node入队同步队列,从而以免重复入队。(这一点需要和后面的中断流程内容联动,如果难以理解,可以直接往下看)
- 如果CAS设置成功,那么enq(node)入队,然后肯定返回true。但是注意,在一定条件下,会唤醒node代表线程。注意enq(node)返回node入队后的前驱prev。
- 这个一定条件是指,node的前驱状态是同步队列节点的取消状态,或者状态<=0但CAS设置前驱状态为SIGNAL失败了。
- 如果上面条件发生了,就直接唤醒node线程。这里我们回想一下await()第一次调用park之后这个时间点,现在node线程终于被唤醒,假设没有中断发生过的话,不会因break退出循环,再一次检测!isOnSyncQueue(node)会发生条件不成立(node已经因为enq(node)而成功入队)。然后又会走到独占锁获得过程中的acquireQueued函数。
- 唤醒node代表线程不一定代表它接下来能够获得锁,但是我们也不用担心这会有什么坏影响,因为acquireQueued函数自己会去做判断,如果发现还是获取不到锁的话,则会调用shouldParkAfterFailedAcquire将node的前驱设置为SIGNAL的。
- 总之,compareAndSetWaitStatus(p, ws, Node.SIGNAL)直接保证了node的前驱状态为SIGNAL,而LockSupport.unpark(node.thread)间接保证了node的前驱状态为SIGNAL,之所以说间接,是因为这不是在signal线程里做的,而是通过唤醒node线程做到的。
简单总结一下signalAll方法:
1、将条件队列清空(通过lastWaiter = firstWaiter = null来达到效果,但函数中的局部变量已经保存了队头,且实际上节点的链接还存在着)。
遍历每个节点。
2、如果遍历节点已经被取消掉了(compareAndSetWaitStatus(node, Node.CONDITION, 0)失败),那么直接返回,处理下一个节点。
3、如果遍历节点还没取消掉(compareAndSetWaitStatus(node, Node.CONDITION, 0)成功),那么将其入队同步队列。在一定条件下(无法设置node前驱状态为SIGNAL时),还将唤醒node代表线程。然后处理下一个节点。
另外注意,signalAll方法直到结束返回,都一直没有释放锁呢(因为没有在signalAll里面执行过release),也就是说,执行signalAll的线程一直都是持有锁的。
3.8、signal流程
相比signalAll
,signal
方法只会唤醒一个node,准确的说,是唤醒从同步队列队头开始的第一个状态为CONDITION
的node。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); //与之前的doSignalAll不同,这里是doSignal
}
private void doSignal(Node first) {
do {
1、nextWaiter设置为null
if ( (firstWaiter = first.nextWaiter) == null)
1.2、若firstWaiter为null,则lastWaiter也设置为null
lastWaiter = null;
2、设置nextWaiter为null
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);//transferForSignal失败,设置新的头节点
}
-
整个逻辑是一个do while循环,不过这个循环一般不会执行多次,只要有一次signal成功了(transferForSignal(first)返回了真)就不会继续循环了,因为这个函数的目的就只是signal一个节点。
-
如果signal失败了,那么获得新队头((first = firstWaiter) != null),只要新队头不为null,则继续下一次循环。
-
相比signalAll流程,我们使用的还是这个transferForSignal来做的signal动作,但这里我们终于使用到了transferForSignal的返回值,如果返回了true说明已经signal了一个node了,也就不用继续循环了。
总之,signal方法会唤醒条件队列中第一个状态不是取消状态(不是CONDITION)的节点。
4、transferForSignal入队与中断线程transferAfterCancelledWait区别
4.1、transferForSignal不唤醒线程
按照上面signal的流程(包括signal和signalAll),假设之前node线程没有被中断过,且执行signalAll的线程不唤醒node线程,那么执行signal流程完毕后此时node的状态为:
-
在数据结构上,node已经离开了条件队列(first.nextWaiter = null),处于了同步队列上了(Node p = enq(node))。
-
在执行过程上,node线程当前还是阻塞在await的while循环LockSupport.park(this)这里。(这一点没有变化)
要想node节点执行,则需要该节点的prev节点调用release方法执行(LockSupport.unpark(s.thread);),唤醒node,从await的while循环处的LockSupport.park(this)开始执行,因没有中断,不会通过break退出循环,然后判断循环条件!isOnSyncQueue(node)
发现不成立而退出循环,继续执行进入acquireQueued再次尝试获取锁,当然不一定成功获取锁,即使不成功会继续阻塞在acquireQueued方法里的shouldParkAfterFailedAcquire方法处。
4.2、transferForSignal唤醒线程
按照上面signal的流程,假设之前node线程没有被中断过,且执行signalAll的线程唤醒node线程,那么执行signal流程完毕后此时node的状态为:
-
在数据结构上,node已经离开了条件队列(first.nextWaiter = null),处于了同步队列上了(Node p = enq(node))。
-
在执行过程上,node线程从LockSupport.park(this)这里被唤醒,不会因为有中断状态而break出循环,然后判断循环条件!isOnSyncQueue(node)发现不成立而退出循环,然后执行acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里,若获取锁成功,说明prev为head节点恰好释放了锁。
4.3、中断流程(有线程将node线程中断,在signal之前)
中断await当前线程的线程终于出马了(现在考虑没有执行signal的线程,或者说中断这个node在signal这个node之前),看看中断流程是怎样的流程。首先要知道,中断await当前线程的线程执行完中断动作后,我们就不用关心它了,剩余动作还是靠node线程自己完成的。
本场景下,中断来临之前,node的状态就和 await()第一次调用park之后 一样:
-
在数据结构上,node在条件队列上(addConditionWaiter)。
-
在执行过程上,node线程当前阻塞在LockSupport.park(this)这里。
首先,中断来了以后,node线程会从LockSupport.park(this)处被唤醒,然后执行checkInterruptWhileWaiting(之前一直没有讲这个函数,是因为在这个流程中它才会真正发挥作用,之前的signal流程它肯定会返回0的,而返回就不会break出循环)。
private int checkInterruptWhileWaiting(Node node) {
//判断贤臣给是否中断过
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
Thread.interrupted()
首先判断当前线程有没有被中断过,如果没有,那么返回0.- 如果有中断过,那么通过
transferAfterCancelledWait(node)
判断返回THROW_IE
还是REINTERRUPT
.
checkInterruptWhileWaiting的返回值最终是会赋值给局部变量interruptMode的,它现在有三种可能值:
1、0:代表整个await过程中没有发生过中断。
THROW_IE:代表await执行完毕返回用户代码处时,需要抛出异常。当中断流程发生在signal流程之前时。
2、REINTERRUPT:代表await执行完毕返回用户代码处时,不需要抛出异常,仅需要重新置上中断状态。当signal流程发生在中断流程之前时。
3、之所以THROW_IE和REINTERRUPT两个值所代表的场景需要进行区分,是因为一个线程A因await而进入condition queue后,正常的流程是另一个线程B执行signal或signalAll后才使得线程A的node入队到sync queue。但如果中断流程发生在signal流程之前,也能使得线程A的node入队到sync queue,但这就没有走正常的流程了。
final boolean transferAfterCancelledWait(Node node) {
1、成功则说明中断唤醒发生再signal之前
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
2、等待signal让node入队
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
首先通过CAS设置node状态从CONDITION变成0,如果成功了,就将node入队同步队列,然后直接返回true。注意,这里的CAS操作和transferForSignal里的compareAndSetWaitStatus(node, Node.CONDITION, 0)是一样的,也就是说:执行signal的线程 和 因中断被唤醒的node线程,在这句compareAndSetWaitStatus(node, Node.CONDITION, 0)这里有竞争关系,谁竞争赢了,谁才有资格将node入队同步队列enq(node)。
这样的竞争关系是很必要的,直接避免两个线程将同一个node重复入队。
通过CAS设置node状态从CONDITION变成0,如果失败了,那就不能再执行enq(node)啦。因为肯定有另一个signal线程正在执行enq(node)或者已经执行完了enq(node)了。
但这里我们如果发现另一个signal线程还没有执行完了enq(node)(通过!isOnSyncQueue(node)条件判断),就必须一直等待,直到另一个signal线程执行完了enq(node),然后循环才可以退出。之所以这么等一下,是因为如果不等,node线程自己接下来就会执行到acquireQueued了,而执行acquireQueued的前提就是已经入队同步队列完毕。
等到node线程执行完了checkInterruptWhileWaiting,考虑本章的中断场景,就会直接break出循环,然后执行到acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里。
总之执行中断流程完毕后此时node的状态为:
-
在数据结构上,node还是处于条件队列上(没有执行first.nextWaiter = null),但同时也处于了同步队列上了(enq(node))。不用担心nextWaiter会影响到当前node不被独占锁的释放而唤醒,因为独占锁的释放过程不关心head后继的nextWaiter。
-
在执行过程上,node线程从LockSupport.park(this)这里被中断唤醒,因为有中断状态而break出循环,然后执行acquireQueued。如果不能获得锁,还是会阻塞在acquireQueued的shouldParkAfterFailedAcquire里。
4.4、因中断被唤醒的node线程 和 signal线程 的竞争关系
上面说了中断流程和signal流程谁在前面,await的表现也会有所不同。具体的说,则体现在:因中断被唤醒的node线程 和 signal线程 的竞争关系上。这两个线程完全有可能同时在执行中,而它们的竞争点则体现在:
因中断被唤醒的node线程。transferAfterCancelledWait函数里的compareAndSetWaitStatus(node, Node.CONDITION, 0)。
signal线程。transferForSignal函数里的compareAndSetWaitStatus(node, Node.CONDITION, 0)。
这两个transfer方法都会执行同一个CAS操作,但很明显,只能有一个线程能够执行CAS操作成功。
竞争成功的那一方,transfer方法会返回true,并且会执行enq(node)。
竞争失败的那一方,transfer方法会返回false,并且不会执行enq(node)。
当一个处于条件队列上的node,状态从CONDITION变成0时,就意味着它正在前往同步队列,或者已经放置在同步队列上了。
如果transferAfterCancelledWait竞争成功,我们称这个node线程走的是中断流程。
如果transferForSignal竞争成功,我们称这个node线程走的是signal流程。
5、终于执行到acquireQueued
讲了半天,终于讲到acquireQueued了。但重点内容其实都在前面,acquireQueued后面的都是一些善后处理而已了。
既然已经执行到了acquireQueued,说明又会走独占锁的获取过程了,在此不赘述了。我们只需要知道,从acquireQueued返回时,node线程已经获取到了锁,并且返回了acquireQueued过程中是否有过中断。注意,这和acquireQueued执行前发生的中断是两个不同的中断,acquireQueued执行前发生的中断会被checkInterruptWhileWaiting消耗掉,并赋值给interruptMode的。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
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);
}
注意,int savedState = fullyRelease(node)之前释放的同步器状态,现在acquireQueued(node, savedState)都要重新全部再获取回来。
acquireQueued期间发生的中断,重要性不如acquireQueued之前发生的中断。假设acquireQueued发生了中断,acquireQueued(node, savedState)则返回true,然后此时interruptMode有三种情况:
- interruptMode为0,说明acquireQueued之前没发生过中断。interruptMode != THROW_IE判断成功。所以需要将interruptMode升级为REINTERRUPT。
- interruptMode为REINTERRUPT,说明acquireQueued之前发生过中断(signal流程在中断流程之前的那种)。interruptMode != THROW_IE判断成功。然后将interruptMode从REINTERRUPT变成REINTERRUPT,这好像是脱裤子放屁,但逻辑这样写就简洁了。
- interruptMode为THROW_IE,说明acquireQueued之前发生过中断(中断流程在signal流程之前的那种)。interruptMode != THROW_IE判断失败。不会去执行interruptMode = REINTERRUPT,因为执行了反而使得中断等级下降了。说到底,还是因为acquireQueued期间发生的中断,重要性不如acquireQueued之前发生的中断。
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
当中断唤醒会执行这儿,清楚所有的不是condition的节点。
最后根据interruptMode
来判断做出不同的中断反应。
6、await()总结
await()对于使用者来说,进入await()时是持有锁的,阻塞后退出await()时也是持有锁的。signal() signalAll()也是一样。
从实现内部的持有锁情况来看:
-
await()在从开头到fullyRelease执行前,是持有锁的。
-
await()在从fullyRelease执行后 到 acquireQueued执行前,是没有持有锁的。
-
await()在 acquireQueued执行后到最后,是持有锁的。
-
signal() signalAll()全程都是持有锁的。
await()的整体流程如下:
- 将当前线程包装成一个node后(Node node = addConditionWaiter()),完全释放锁(int savedState = fullyRelease(node))。
- 当前线程阻塞在LockSupport.park(this)处,等待signal线程或者中断线程的到来。
- 被唤醒后,到达acquireQueued之前,当前线程的node已经置于sync queue之上了。
- 执行acquireQueued,进行阻塞式的抢锁。
- 退出acquireQueued时,当前线程已经重新获得了锁,之后进行善后工作。