文章目录
讲了这么基于AQS和Lock实现的同步锁机制,我们可以发现它们主要是实现了同步并发的控制
- lock时获取锁,失败则进入队列等待。成功则自动向下执行
- unLock时释放锁,自动触发唤醒下一个线程。 (基于LockSuport实现阻塞线程的唤醒以继续自旋竞争锁过程)
我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停同时释放锁;等到线程B修改了条件condition,使condition满足了线程A的要求时,A再继续执行。
在这里我们把这个定义为等待通知的控制
两者都会引发线程的阻塞,但是同步并发的控制的阻塞是由于缺失锁资源,并被放置到[AQS同步队列]; 而等待通知的控制的阻塞,则是由于缺少某种业务条件的主动阻塞,并被放置到[Condition等待队列]
后文讲到的await()
和singal()
其实就是对这两个队列的修改和LockPark操作
一、等待通知
1.1 自旋实现的等待通知
最简单的实现方法就是将condition设为一个volatile的变量,当A线程检测到条件不满足时就自旋,类似下面:
public class Test {
private static volatile int condition = 0;
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
while (!(condition == 1)) {
// 条件不满足,自旋
}
System.out.println("a executed");
}
});
A.start();
Thread.sleep(2000);
condition = 1;
}
}
这种方式的问题在于自旋非常耗费CPU资源,当然如果在自旋的代码块里加入Thread.sleep(time)将会减轻CPU资源的消耗,但是如果time设的太大,A线程就不能及时响应condition的变化,如果设的太小,依然会造成CPU的消耗。
1.2 Object提供的等待通知
因此,java在Object类里提供了wait()和notify()方法,使用方法如下:
class Test1 {
private static volatile int condition = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
while (!(condition == 1)) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("a executed by notify");
}
}
});
A.start();
Thread.sleep(2000);
condition = 1;
synchronized (lock) {
lock.notify();
}
}
}
通过代码可以看出,在使用一个对象的wait()、notify()方法前必须要获取这个对象的锁。
- 当线程A调用了lock对象的wait()方法后,线程A将释放持有的lock对象的锁,然后将自己挂起,直到有其他线程调用notify()/notifyAll()方法或被中断。
- 可以看到在lock.wait()前面检测condition条件的时候使用了一个while循环而不是if,那是因为当有其他线程把condition修改为满足A线程的要求并调用notify()后,A线程会重新等待获取锁,获取到锁后才从lock.wait()方法返回,而在A线程等待锁的过程中,condition是有可能再次变化的。
因为wait()、notify()是和synchronized配合使用的,因此如果使用了显示锁Lock,就不能用了。
所以显示锁要提供自己的等待/通知机制,Condition应运而生。
1.3 显示锁提供的等待通知
我们用Condition实现上面的例子:
class Test2 {
private static volatile int condition = 0;
private static Lock lock = new ReentrantLock();
private static Condition lockCondition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();//【竞争锁】
try {
while (!(condition == 1)) {
lockCondition.await();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
System.out.println("a executed by condition");
}
});
A.start();
Thread.sleep(2000);
condition = 1;
lock.lock();//【竞争锁】
try {
lockCondition.signal();
} finally {
lock.unlock();
}
}
}
可以看到通过 lock.newCondition() 可以获得到 lock 对应的一个Condition对象lockCondition ,lockCondition的await()、signal()方法分别对应之前的Object的wait()和notify()方法。整体上和Object的等待通知是类似的。
二、应用举例
上面我们看到了Condition实现的等待通知和Object的等待通知是非常类似的,而Condition提供的等待通知功能更强大
最重要的一点是,一个lock对象可以通过多次调用 lock.newCondition() 获取多个Condition对象,也就是说,在一个lock对象上,可以有多个等待队列,而Object的等待通知在一个Object上,只能有一个等待队列。
用下面的例子说明,下面的代码实现了一个阻塞队列,当队列已满时,add操作被阻塞有其他线程通过remove方法删除元素;当队列已空时,remove操作被阻塞直到有其他线程通过add方法添加元素。
public class BoundedQueue1<T> {
public List<T> q; //这个列表用来存队列的元素
private int maxSize; //队列的最大长度
private Lock lock = new ReentrantLock();
private Condition addConditoin = lock.newCondition();
private Condition removeConditoin = lock.newCondition();
public BoundedQueue1(int size) {
q = new ArrayList<>(size);
maxSize = size;
}
public void add(T e) {
lock.lock();
try {
while (q.size() == maxSize) {
addConditoin.await();
}
q.add(e);
removeConditoin.signal(); //执行了添加操作后唤醒因队列空被阻塞的删除操作
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public T remove() {
lock.lock();
try {
while (q.size() == 0) {
removeConditoin.await();
}
T e = q.remove(0);
addConditoin.signal(); //执行删除操作后唤醒因队列满而被阻塞的添加操作
return e;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
lock.unlock();
}
}
}
Condition总是唤醒等待在自己这个条件上的 waiter线程队列
三、源码分析
3.1 概述
前我们介绍AQS的时候说过,AQS的同步排队用了一个隐式的双向队列,同步队列的每个节点是一个AbstractQueuedSynchronizer.Node实例。
static final class Node {
//等待状态
volatile int waitStatus;
//当前转换为Node节点的线程。
volatile Thread thread;
//当前节点在同步队列中的上一个节点。
volatile Node prev;
//当前节点在同步队列中的下一个节点。
volatile Node next;
//Node既可以作为同步队列节点(竞争使用共享资源的线程)使用,也可以作为Condition的等待队列节点(类似与调用Obect.wait等待线程)使用(将会在后面讲Condition时讲到)。
//在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;
//在作为等待队列节点使用时,nextWaiter保存后继节点。
Node nextWaiter;
}
等待状态主要包含以下状态
状态 | 值 | 含义 |
---|---|---|
CANCELLED | 1 | 被中断或获取同步状态超时的线程将会被置为该状态,且该状态下的线程不会再阻塞。 |
SIGNAL | -1 | 线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark) |
CONDITION | -2 | 标识当前节点是作为等待队列节点使用的。当前节点在Condition中的等待队列上,(关于Condition会在下篇文章进行介绍),其他线程调用了Condition的singal()方法后,该节点会从等待队列转移到AQS的同步队列中,等待获取同步锁。 |
PROPAGATE | -3 | 与共享式获取同步状态有关,该状态标识的节点对应线程处于可运行的状态。 |
0 | 0 | 初始状态 |
Condition实现等待的时候内部也有一个等待队列,等待队列是一个隐式的单向队列,等待队列中的每一个节点也是一个AbstractQueuedSynchronizer.Node实例。
每个Condition对象中保存了firstWaiter和lastWaiter作为队列首节点和尾节点,每个节点使用Node.nextWaiter保存下一个节点的引用,因此等待队列是一个单向队列。
- 每当一个线程调用Condition.await()方法,那么该线程会释放锁,构造成一个Node节点加入到等待队列的队尾。
- 每当一个线程调用Condition.signal()方法,那么该线程会等待队列的队尾节点移到AQS中
3.2 整体结构
ConditionObject是AQS的一个内部类,因此我们在调用lock.newCondition();
获得一个Condition实例
时,该实例是可以直接操控AQS中的对象和函数的,譬如state和队列的访问
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {
//TODO
}
}
3.3 等待await
Condition.await()方法的源码如下:
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//【1】根据当前线程,构造一个[新的等待队列Node加入到Condition等待队列队尾]
Node node = addConditionWaiter();
//【2】释放当前线程的独占锁,不管重入几次,都把state释放为0
int savedState = fullyRelease(node);
int interruptMode = 0;
//【3核心】如果当前节点没有在[AQS同步队列]上,即还没有被signal,则将进入循环体以阻塞当前线程
// ps: 被signal通知时会被放到[AQS同步队列]中,退出该循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//和中断相关的,主要是区分两种中断:
// 是在被signal前中断还是在被signal后中断,如果是被signal前就被中断则抛出 InterruptedException,否则执行 Thread.currentThread().interrupt();
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //被中断则直接退出自旋
break;
}
//【4】退出了上面自旋说明当前节点已经在同步队列上,但是当前节点不一定在同步队列队首。
//1. acquireQueued将阻塞直到当前节点成为队首,即当前线程获得了锁。
//2. 然后await()方法就可以退出了,让线程继续执行await()后的代码。
//体现了调用signal后,当前线程并不是理解被唤醒执行,而是等待到锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
}
主要步骤如下:
- 根据当前线程,构造一个[新的等待队列Node加入到Condition等待队列队尾]
- 释放当前线程的独占锁,不管重入几次,都把state释放为0
- 调用
tryRelease()
- 调用
unparkSuccessor()
, 转移锁的持有权利
- 调用
- 【核心】如果当前节点没有在[AQS同步队列]上(即还没有被signal)则将进入循环体以阻塞当前线程
await()
时,加入了[Condition等待队列队尾],因此会阻塞- 被
signal()通知
时, 会被放到[AQS同步队列]中,导致退出该自旋式循环
- 退出该自旋式循环后(证明已经被通知,移到[AQS同步队列]中),采用类似
acquire()
的方式,自旋式竞争锁 和 阻塞自己并修改pre- 获取到锁,则继续执行
- 未获取到,则阻塞自己,等待锁资源的释放时被另一个线程的
release()
唤醒
步骤4也体现了体现了调用signal后,当前线程并不是理解被唤醒执行,而是等待到锁
其中有用到一些AQS方法,我们也写出来:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/*****************************释放当前线程的独占锁,不管重入几次,都把state释放为0********************************/
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;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//独占式释放同步状态,成功返回true,失败返回false。
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**********************当前节点没有在同步队列上,即还没有被signal*******************************************/
final boolean isOnSyncQueue(Node node) {
//如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在Condition等待队列上而不是AQS同步队列上。
//之所以可以用node.prev来判断,是因为一个节点如果要加入Condition同步队列,在加入前就会设置好prev字段。
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;//进入循环体继续循环
//如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
if (node.next != null) // If has successor, it must be on queue
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;
}
}
/**********************自旋式竞争锁 和 阻塞自己并修改pre*******************************************/
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
}
3.4 通知signal
Condition.signal() 方法的源码如下:
public class ConditionObject implements Condition, java.io.Serializable {
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException(); //如果同步状态不是被当前线程独占,直接抛出异常。从这里也能看出来,Condition只能配合独占类同步组件使用。
Node first = firstWaiter;
if (first != null)
doSignal(first); //通知{Condition等待队列}的队首节点。
}
}
让我们来看看 doSignal
到底做了什么:
public class ConditionObject implements Condition, java.io.Serializable {
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒当前节点的后继节点。
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;
//如果当前状态不为CONDITION,说明该节点等待已被中断,则该方法返回false,doSignal()方法会继续尝试唤醒当前节点的后继节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//【1】将节点加入同步队列,返回的p是节点在同步队列中的先驱节点.此时await()操作的步骤3的解锁条件就已经开启了
Node p = enq(node);
int ws = p.waitStatus;
//【2】
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
}
我们主要看transferForSignal
:
- 【1】[对Condition队首节点的处理]将Condition队首节点加入同步队列,返回的p是节点在同步队列中的先驱节点.此时await()操作的步骤3的解锁条件就已经开启了
- 但是注意: 目标节点还是处于阻塞状态,也就是说退出循环的条件有了,但是循环还未开始
- 【2】[对自己的处理]
- 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒[Condition队首节点]对应的线程.
- 此时await()方法就会完成步骤3,进入步骤4.
- 步骤4:线程被唤醒后会执行acquireQueued方法,该方法会重新尝试将节点的先驱状态设为SIGNAL并再次park线程;
- 如果当前设置前驱节点状态为SIGNAL成功,那么就阻塞自己[Condition队首节点].
- 因为不需要马上唤醒[Condition队首节点]了,当[Condition队首节点]的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
- 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒[Condition队首节点]对应的线程.
对于【2】这里不加这个判断条件应该也是可以的。
只是对于CAS修改前驱节点状态为SIGNAL成功这种情况来说,如果不加这个判断条件,提前唤醒了线程,等进入acquireQueued方法了节点发现自己的前驱不是首节点,还要再阻塞,等到其前驱节点成为首节点并释放锁时再唤醒一次;
而如果加了这个条件,线程被唤醒的时候它的前驱节点肯定是首节点了,线程就有机会直接获取同步状态从而避免二次阻塞,节省了硬件资源。
四、Condition等待通知的本质
“同步并发的控制”、”等待通知的控制“两者都会引发线程的阻塞,但是同步并发的控制的阻塞是由于缺失锁资源,并被放置到[AQS同步队列]; 而等待通知的控制的阻塞,则是由于缺少某种业务条件的主动阻塞,并被放置到[Condition等待队列]
await()
和 singal()
的本质就是等待队列和同步队列的交互
当一个持有锁的线程调用Condition.await()时,它会执行以下步骤:
- 构造一个新的等待队列节点加入到等待队列队尾
- 释放锁,也就是将它的同步队列节点从同步队列队首移除
- 自旋,直到它在等待队列上的节点移动到了同步队列(通过其他线程调用signal())或被中断
- 阻塞当前节点,以阻塞线程(当然,也阻塞了自旋)
- 跳出自旋
- 退出该自旋式循环后(证明已经被通知,移到[AQS同步队列]中),采用类似
acquire()
的方式,自旋式竞争锁 和 阻塞自己并修改pre- 获取到锁,则继续执行
- 未获取到,则阻塞自己,等待锁资源的释放时被另一个线程的
release()
唤醒
当一个持有锁的线程调用Condition.signal()时,它会执行以下操作:
- 从等待队列的队首开始,尝试对队首节点执行唤醒操作;如果节点CANCELLED,就尝试唤醒下一个节点;如果再CANCELLED则继续迭代。
- 对每个节点执行唤醒操作时,首先将节点加入同步队列,此时await()操作的步骤3的退出自旋条件就已经开启了。
- 但是注意: 目标节点还是处于阻塞状态,也就是说退出循环的条件有了,但是循环还未开始
- [对当前线程的处理]
- 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒[Condition队首节点]对应的线程.
- 此时await()方法就会完成步骤3,进入步骤4.
- 步骤4:线程被唤醒后会执行acquireQueued方法,该方法会重新尝试将节点的先驱状态设为SIGNAL并再次park线程;
- 如果当前设置前驱节点状态为SIGNAL成功,那么就阻塞自己[Condition队首节点].
- 因为不需要马上唤醒[Condition队首节点]了,当[Condition队首节点]的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
- 如果先驱节点的状态为CANCELLED(>0) 或设置先驱节点的状态为SIGNAL失败,那么就立即唤醒[Condition队首节点]对应的线程.
五、总结
如果知道Object的等待通知机制,Condition的使用是比较容易掌握的,因为和Object等待通知的使用基本一致。
对Condition的源码理解,主要就是理解等待队列,等待队列可以类比同步队列,而且等待队列比同步队列要简单,因为等待队列是单向队列,同步队列是双向队列。
之所以同步队列要设计成双向的,是
- 因为在同步队列中,节点唤醒是接力式的,由每一个节点唤醒它的下一个节点
- 如果是由next指针获取下一个节点,是有可能获取失败的,因为虚拟队列每添加一个节点,是先用CAS把tail设置为新节点,然后才修改原tail的next指针到新节点的。因此用next向后遍历是不安全的
- 但是如果在设置新节点为tail前,为新节点设置prev,则可以保证从tail往前遍历是安全的。
因此要安全的获取一个节点Node的下一个节点,先要看next是不是null,如果是null,还要从tail往前遍历看看能不能遍历到Node。
等待队列就简单多了,等待的线程就是等待者,只负责等待,唤醒的线程就是唤醒者,只负责唤醒,因此每次要执行唤醒操作的时候,直接唤醒等待队列的首节点就行了。等待队列的实现中不需要遍历队列,因此也不需要prev指针。