Condition

Catalogue
  1. 1. await
  2. 2. Signal

Condition接口定义的几个方法如下:

image-20181029220107248

其中,await方法类似于Object的wait方法,而signal和signalAll则分别对应于notify和notifyAll方法。

Condition其实现类在AQS中以内部类ConditionObject的形式存在。condition由锁的方法Lock.newCondition()来获取,且一个Lock可获取多个Condition,而不像Object只能设置一个条件。

下面说一下ConditionObject的await和signal方法。

await

在调用该方法时,当前线程需要获取到与该Condition关联的锁,如果不加锁进入该方法,可能会导致其他线程调用signal方法时,还没进入到await方法,导致这个信号丢失,而不起任何效果。调用该方法后,线程释放锁,进入睡眠状态,在以下四种情况下线程会被唤醒

  • 其他线程调用signal方法,当前线程恰好被选中唤醒

  • 其他线程调用signalAll方法

  • 被其他线程中断

    中断是让线程停止当前所做的事去做别的事情的一个标志。停止当前所做的事不代表线程就占用CPU在做什么,线程在睡眠也叫做线程所做的事。调用Thread.interrupt()方法可以设置目标线程的中断状态,至于线程如何响应中断,则看程序是如何写的。

  • 发生虚假唤醒(spurious wakeup)

虚假唤醒时指线程在没有收到线程唤醒信号的时候醒了过来。await方法在jvm执行时实质是调用了底层pthread_cond_wait/pthread_cond_timedwait函数,而pthread_cond_wait可能在没收到信号时返回。同时,由于编码不规范也会产生虚假唤醒,举个例子

线程A获取锁,发现队列空,因此wait,线程B向队列插入数据,改变条件变量,发出signal,线程C获取到锁,将数据移除,等到A获取锁从wait状态醒来时,应该是去队列移除数据的,但是此时的队列已经为空,这个唤醒是虚假唤醒。

pthread_cond_wait的作者认为既然编程不规范也会导致虚假唤醒,那么就交由上层来避免虚假唤醒,而不是底层来实现。

由于虚假唤醒的存在,await方法应该在循环中调用。

await的基本流程如下:

  1. 生成并添加代表线程的Node节点到条件队列
  2. 释放锁
  3. 阻塞直到Node被移到同步队列
  4. 重新获取锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//新节点入条件队列
Node node = addConditionWaiter();
//当前线程已持有锁,但由于要被阻塞,为不影响其他线程,需要先释放锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 阻塞直到Node被移到锁队列
while (!isOnSyncQueue(node)) {
// 阻塞
LockSupport.park(this);
/**
* 检测中断,一旦发生中断
* 1.将条件队列中因中断而唤醒的节点进行转移(注意此处是中断)
* 2.退出循环 -> 接下来会在循环外进行中断处理
*/
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 重新尝试获取同步锁,获取成功后且被中断,当中断模式为抛出异常时,需要设置为重新中断,补充:acquireQueued会返回获取锁过程中线程是否有过中断,true则说明发生过中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 若当前节点存在后继节点时,需要执行出队操作
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//interruptMode != 0 说明是需要进行中断处理的
if (interruptMode != 0)
//执行中断处理
reportInterruptAfterWait(interruptMode);
}

Signal

signal做的事情

  1. 清除节点: 从条件队列中移除第一个节点
  2. 节点转移: 将条件节点转换为同步节点,即从条件队列转移到同步队列
  3. 唤醒节点: 将转移成功的节点重新唤醒

当cancellation(超时或者中断)和signal几乎同时发生时,会存在竞争,根据Java规范,如果中断先发生,await在重新获取到锁后,抛出InterruptException,如果中断后于signal,则不抛出异常,只需修改线程中断的标志位。