要看懂这个,必须要先看懂上一篇关于 java并发--AQS【简介】
,java并发--独占锁
的介绍,或者你已经有相关的知识了,否则这节肯定是看不懂的。
Condition使用
我们先来看看 Condition 的使用场景,Condition 经常可以用在生产者-消费者的场景中,请看 Doug Lea 给出的这个例子:
1 | import java.util.concurrent.locks.Condition; |
我们可以看到,在使用 condition 时,必须先持有相应的锁。这个和 Object 类中的方法有相似的语义,需要先持有某个对象的监视器锁才可以执行 wait(), notify() 或 notifyAll() 方法。
obj.wait(),obj.notify() 或 obj.notifyAll() 是基于对象的监视器锁的。而这里说的 Condition 是基于 ReentrantLock 实现的, ReentrantLock 是依赖于 AbstractQueuedSynchronizer 实现的。
在往下看之前,读者心里要有一个整体的概念。condition 是依赖于 ReentrantLock 的,不管是调用 await 进入等待还是 signal 唤醒,都必须获取到锁才能进行操作。
AQS中Condition的实现
每个 ReentrantLock 实例可以通过调用多次 newCondition 产生多个 ConditionObject 的实例:
1 | final ConditionObject newCondition() { |
我们首先来看下我们关注的 Condition 的实现类 AbstractQueuedSynchronizer
类中的 ConditionObject
。
1 | public class ConditionObject implements Condition, java.io.Serializable { |
在上一篇介绍 AQS 的时候,我们有一个阻塞队列,用于保存等待获取锁的线程的队列。这里我们引入另一个概念,叫条件队列(condition queue)。
"这里的阻塞队列如果叫做同步队列(sync queue)其实比较贴切,不过为了和前篇呼应,我就继续使用阻塞队列了。记住这里的两个概念,阻塞队列和条件队列。
这里,我们简单回顾下 Node 的属性:
1
2
3
4
5
6 > volatile int waitStatus; // 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)
> volatile Node prev;
> volatile Node next;
> volatile Thread thread;
> Node nextWaiter;
>
prev 和 next 用于实现阻塞队列的双向链表,这里的 nextWaiter 用于实现条件队列的单向链表
基本上,把这张图看懂,你也就知道 condition 的处理流程了。所以,我先简单解释下这图,然后再具体地解释代码实现。
- 条件队列和阻塞队列的节点,都是 Node 的实例,条件队列的节点是需要转移到阻塞队列中去的;
- 我们知道一个 ReentrantLock 实例可以通过多次调用 newCondition() 来产生多个 Condition 实例。注意,ConditionObject 只有两个属性 firstWaiter 和 lastWaiter;
- 每个 condition 有一个关联的条件队列,例如线程 1 调用
condition1.await()
方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行,条件队列是一个单向链表; - 调用
condition1.signal()
触发一次唤醒,此时唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter(队头) 移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行。
上面的 2->3->4 描述了一个最简单的流程,没有考虑中断、signalAll、还有带有超时参数的 await 方法等,不过把这里弄懂是这节的主要目的。
-1.线程await,signal流程图
"0. 线程进入等待状态
接下来,我们一步步按照流程来走代码分析,我们先来看看 wait 方法:
1 | // 首先,这个方法是可被中断的,不可被中断的是另一个方法 awaitUninterruptibly() |
其实,我大体上也把整个 await 过程说得十之八九了,下面我们分步把上面的几个点用源码说清楚。
1. 将节点加入到条件队列
addConditionWaiter() 是将当前节点加入到条件队列,这种条件队列内的操作是线程安全的。
"1 | // 将当前线程对应的节点入队,插入队尾 |
上面的这块代码很简单,就是将当前线程进入到条件队列的队尾。
在addWaiter 方法中,有一个 unlinkCancelledWaiters() 方法,该方法用于清除队列中已经取消等待的节点。
当 await 的时候如果发生了取消操作(这点之后会说),或者是在节点入队的时候,发现最后一个节点是被取消的,会调用一次这个方法。
1 | // 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去 |
2. 完全释放独占锁
回到 wait 方法,节点入队了以后,会调用 int savedState = fullyRelease(node);
方法释放锁,注意,这里是完全释放独占锁(fully release),因为 ReentrantLock 是可以重入的。
考虑一下这里的 savedState。如果在 condition1.await() 之前,假设线程先执行了 2 次 lock() 操作,那么 state 为 2,我们理解为该线程持有 2 把锁,这里 await() 方法必须将 state 设置为 0,然后再进入挂起状态,这样其他线程才能持有锁。当它被唤醒的时候,它需要重新持有 2 把锁,才能继续下去。
1 | // 首先,我们要先观察到返回值 savedState 代表 release 之前的 state 值 |
考虑一下,如果一个线程在不持有 lock 的基础上,就去调用 condition1.await() 方法,它能进入条件队列,但是在上面的这个方法中,由于它不持有锁,release(savedState) 这个方法肯定要返回 false,进入到异常分支,然后进入 finally 块设置
node.waitStatus = Node.CANCELLED
,这个已经入队的节点之后会被后继的节点”请出去“。
3. 等待进入阻塞队列
释放掉锁以后,接下来是这段,这边会自旋,如果发现自己还没到阻塞队列,那么挂起,等待被转移到阻塞队列。
1 | int interruptMode = 0; |
isOnSyncQueue(Node node) 用于判断节点是否已经转移到阻塞队列了:
1 | // signal 的时候需要将节点从条件队列移到阻塞队列,这个方法就是判断 node 是否已经移动到阻塞队列了 |
回到前面的循环,isOnSyncQueue(node) 返回 false 的话,那么进到 LockSupport.park(this);
这里线程挂起。
4. signal 唤醒线程,转移到阻塞队列
为了大家理解,这里我们先看唤醒操作,因为刚刚到 LockSupport.park(this);
把线程挂起了,等待唤醒。
1 | // 唤醒等待了最久的线程 |
正常情况下,ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)
这句中,ws <= 0,而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL)
会返回 true,所以一般也不会进去 if 语句块中唤醒 node 对应的线程。然后这个方法返回 true,也就意味着 signal 方法结束了,节点进入了阻塞队列。
假设发生了阻塞队列中的前驱节点取消等待,或者 CAS 失败,只要唤醒线程,让其进到下一步即可。
5. 唤醒后检查中断状态
上一步 signal 之后,我们的线程由条件队列转移到了阻塞队列,之后就准备获取锁了。只要重新获取到锁了以后,继续往下执行。
等线程从挂起中恢复过来,继续往下看
1 | int interruptMode = 0; |
先解释下 interruptMode。interruptMode 可以取值为 REINTERRUPT(1),THROW_IE(-1),0
- REINTERRUPT: 代表 await 返回的时候,需要重新设置中断状态
- THROW_IE: 代表 await 返回的时候,需要抛出 InterruptedException 异常
- 0 :说明在 await 期间,没有发生中断
有以下三种情况会unpark
,让 LockSupport.park(this);
这句返回:
- 常规路径。lock->signal ->转移节点到阻塞队列 -> unlock(release->unparkSuccessor->unpark,在unlock的时候会唤醒被park的线程)
- 线程中断。在 park 的时候,另外一个线程对这个线程进行了中断
- signal 的时候我们说过,转移以后的前驱节点取消了,或者对前驱节点的CAS操作失败了
- 假唤醒。这个也是存在的,和 Object.wait() 类似,都有这个问题
线程唤醒后第一步是调用 checkInterruptWhileWaiting(node) 这个方法,此方法用于判断是否在线程挂起期间发生了中断,如果发生了中断,是 signal 调用之前中断的,还是 signal 之后发生的中断。
1 | // 1. 如果在 signal 之前已经中断,返回 THROW_IE |
Thread.interrupted():如果当前线程已经处于中断状态,那么该方法返回 true,同时将中断状态重置为 false,所以,才有后续的
重新中断(REINTERRUPT)
的使用。
看看怎么判断是 signal 之前还是之后发生的中断:
1 | // 只有线程处于中断状态,才会调用此方法 |
这里再说一遍,即使发生了中断,节点依然会转移到阻塞队列。
到这里,大家应该都知道这个 while 循环怎么退出了吧。要么中断,要么转移成功。
这里描绘了一个场景,本来有个线程,它是排在条件队列的后面的,但是因为它被中断了,那么它会被唤醒,然后它发现自己不是被 signal 的那个,但是它会自己主动去进入到阻塞队列。
6. 获取独占锁
while 循环出来以后,下面是这段代码:
1 | if (acquireQueued(node, savedState) && interruptMode != THROW_IE) |
由于 while 出来后,我们确定节点已经进入了阻塞队列,准备获取锁。
这里的 acquireQueued(node, savedState) 的第一个参数 node 之前已经经过 enq(node) 进入了队列,参数 savedState 是之前释放锁前的 state,这个方法返回的时候,代表当前线程获取了锁,而且 state == savedState了。
注意,前面我们说过,不管有没有发生中断,都会进入到阻塞队列,而 acquireQueued(node, savedState) 的返回值就是代表线程是否被中断。如果返回 true,说明被中断了,而且 interruptMode != THROW_IE,说明在 signal 之前就发生中断了,这里将 interruptMode 设置为 REINTERRUPT,用于待会重新中断。
继续往下:
1 | if (node.nextWaiter != null) // clean up if cancelled |
本着一丝不苟的精神,这边说说 node.nextWaiter != null
怎么满足。我前面也说了 signal 的时候会将节点转移到阻塞队列,有一步是 node.nextWaiter = null,将断开节点和条件队列的联系。
可是,在判断发生中断的情况下,是 signal 之前还是之后发生的?
这部分的时候,我也介绍了,如果 signal 之前就中断了,也需要将节点进行转移到阻塞队列,这部分转移的时候,是没有设置 node.nextWaiter = null 的。
之前我们说过,如果有节点取消,也会调用 unlinkCancelledWaiters 这个方法,就是这里了。
7. 处理中断状态
到这里,我们终于可以好好说下这个 interruptMode 干嘛用了。
- 0:什么都不做,没有被中断过;
- THROW_IE:await 方法抛出 InterruptedException 异常,因为它代表在 await() 期间发生了中断;
- REINTERRUPT:重新中断当前线程,因为它代表 await() 期间没有被中断,而是 signal() 以后发生的中断
1 | private void reportInterruptAfterWait(int interruptMode) |
这个中断状态这部分内容,大家应该都理解了吧,不理解的话,多看几遍就是了。
* 带超时机制的 await
经过前面的 7 步,整个 ConditionObject 类基本上都分析完了,接下来简单分析下带超时机制的 await 方法。
1 | public final long awaitNanos(long nanosTimeout) |
这三个方法都差不多,我们就挑一个出来看看吧:
1 | public final boolean await(long time, TimeUnit unit) |
超时的思路还是很简单的,不带超时参数的 await 是 park,然后等待别人唤醒。而现在就是调用 parkNanos 方法来休眠指定的时间,醒来后判断是否 signal 调用了,调用了就是没有超时,否则就是超时了。超时的话,自己来进行转移到阻塞队列,然后抢锁。
* 不抛出 InterruptedException 的 await
关于 Condition 最后一小节了。
1 | public final void awaitUninterruptibly() { |
很简单,贴一下代码大家就都懂了,我就不废话了。