Java并发:Condition详解

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

重要入口方法

======

Condition的实现主要包括:条件队列、等待和通知。其中条件队列放的是AQS里的Node数据结构,使用nextWaiter来维护条件队列。等待和通知共有7个方法。

  • signal():唤醒该条件队列的头节点。

  • signalAll():唤醒该条件队列的所有节点。

  • awaitUninterruptibly():等待,此方法无法被中断,必须通过唤醒才能解除阻塞。

  • await():当前线程进入等待。

  • awaitNanos(long):当前线程进入等待,有超时时间,入参的单位为纳秒。

  • awaitUntil(Date):当先线程进入等待,直到当前时间超过入参的时间。

  • await(long, TimeUnit):当前线程进入等待,有超时时间,入参可以自己设置时间单位。

这些方法其实大同小异,因此本文只对常用的signal()、signalAll()和await()方法展开详解。搞懂了这3个方法,搞懂其他几个方法也基本没什么阻碍。

基础属性

====

Condition的实现是ConditionObject,而ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为条件队列),该队列是Condition对象实现等待/通知功能的关键。

private static final long serialVersionUID = 1173984872572414699L;

/** First node of condition queue. */

private transient Node firstWaiter; // 条件队列的头节点

/** Last node of condition queue. */

private transient Node lastWaiter; // 条件队列的尾节点

/**

  • Creates a new {@code ConditionObject} instance.

*/

public ConditionObject() { }

通过源码可知,条件队列的节点使用的是AQS的Node数据结构。(Node的数据结构见:Node的数据结构

另外,由于ConditionObject是AQS的内部类,因此必然和AQS是有很多关联的,因此看本文之前必须先了解AQS的实现原理。(如果你对AQS不熟悉,可以参考我的另一篇文章:Java并发:AbstractQueuedSynchronizer详解(独占模式)

条件队列的基本数据结构如下图中的“条件队列”:

await方法

=======

public final void await() throws InterruptedException { // 阻塞当前线程,直接被唤醒或被中断

if (Thread.interrupted()) // 如果当前线程被中断过,则抛出中断异常

throw new InterruptedException();

Node node = addConditionWaiter(); // 添加一个waitStatus为CONDITION的节点到条件队列尾部

int savedState = fullyRelease(node); // 释放操作。我们知道只有在拥有锁(acquire成功)的时候才能调用await()方法,因此,调用await()方法的线程的节点必然是同步队列的头节点。所以,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的条件队列中。

int interruptMode = 0; // 0为正常,被中断值为THROW_IE或REINTERRUPT

while (!isOnSyncQueue(node)) { // isOnSyncQueue:判断node是否在同步队列(注意和条件队列区分。调用signal方法会将节点从条件队列移动到同步队列,因此这边就可以跳出while循环)

LockSupport.park(this); // node如果不在同步队列则进行park(阻塞当前线程)

if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 检查线程被唤醒是否是因为被中断,如果是则跳出循环,否则会进行下一次循环,因为被唤醒前提是进入同步队列,所以下一次循环也必然会跳出循环

break;

}

if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // acquireQueued返回true代表被中断过,如果中断模式不是THROW_IE,则必然为REINTERRUPT(见上面的checkInterruptWhileWaiting方法)

interruptMode = REINTERRUPT;

if (node.nextWaiter != null) // clean up if cancelled

unlinkCancelledWaiters(); // 移除waitStatus为CANCELLED的节点

if (interruptMode != 0) // 如果跳出while循环是因为被中断

reportInterruptAfterWait(interruptMode); // 则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程

}

  1. 如果当前线程被中断过,则抛出中断异常。

  2. 调用addConditionWaiter方法(详解见下文addConditionWaiter方法)添加一个waitStatus为CONDITION的节点到条件队列尾部。

  3. 调用fullyRelease方法(详解见下文fullyRelease方法)释放锁。

  4. 调用isOnSyncQueue方法(详解见下文isOnSyncQueue方法)来阻塞线程,直到被唤醒或被中断。

  5. 调用acquireQueued方法(详解见acquireQueued方法详解)来尝试获取锁,并判断线程跳出while循环是被唤醒还是被中断。

  6. 如果跳出while循环是因为被中断,则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程。

addConditionWaiter方法


private Node addConditionWaiter() { // 添加一个waitStatus为CONDITION的节点到条件队列尾部

Node t = lastWaiter;

// If lastWaiter is cancelled, clean out.

if (t != null && t.waitStatus != Node.CONDITION) {

unlinkCancelledWaiters(); // 移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION)

t = lastWaiter; // 将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)

}

Node node = new Node(Thread.currentThread(), Node.CONDITION); // 以当前线程新建一个waitStatus为CONDITION的节点

if (t == null) // t为空,代表条件队列为空

firstWaiter = node; // 将头节点赋值为node

else

t.nextWaiter = node; // 否则,队列不为空。将t(原尾节点)的后继节点赋值为node

lastWaiter = node; // 将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的

return node;

}

  1. 如果条件队列的尾节点不为null并且waitStatus不为CONDITION,则调用unlinkCancelledWaiters方法(详解见下文unlinkCancelledWaiters方法)移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION),并将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)

  2. 以当前线程新建一个waitStatus为CONDITION的节点。

  3. 如果t为空,代表条件队列为空,将头节点赋值为node;否则,队列不为空。将t(原尾节点)的后继节点赋值为node。

  4. 最后将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

unlinkCancelledWaiters方法


private void unlinkCancelledWaiters() { // 从条件队列移除所有waitStatus不为CONDITION的节点

Node t = firstWaiter; // t赋值为条件队列的尾节点

Node trail = null;

while (t != null) {

Node next = t.nextWaiter; // 向下遍历

if (t.waitStatus != Node.CONDITION) { // 如果t的waitStatus不为CONDITION

t.nextWaiter = null; // 断开t与t后继节点的关联

if (trail == null) // 如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点

firstWaiter = next;

else

trail.nextWaiter = next; // 否则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)

if (next == null)

lastWaiter = trail;

}

else

trail = t; // 如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION

t = next; // t指向下一个节点

}

}

  1. 将t赋值为条件队列的尾节点 。

  2. 从t开始遍历整个条件队列。

  3. 如果t的waitStatus不为CONDITION,则断开t与t后继节点的关联。

  4. 如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点。

  5. 如果trail不为null,则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)

  6. 如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION。

  7. 最后将 t指向下一个节点,准备开始下一次循环。

例子图解过程:

fullyRelease方法


final int fullyRelease(Node node) { // 释放锁

boolean failed = true;

try {

int savedState = getState(); // 当前的同步状态

if (release(savedState)) { // 独占模式下release(一般指释放锁)

failed = false;

return savedState;

} else {

throw new IllegalMonitorStateException();

}

} finally {

if (failed)

node.waitStatus = Node.CANCELLED; // 如果release失败则将该节点的waitStatus设置为CANCELLED

}

}

调用release方法(详解见release方法详解)释放锁,如果释放失败,则将该节点的waitStatus设置为CANCELLED。

isOnSyncQueue方法


final boolean isOnSyncQueue(Node node) { // 判断node是否再同步队列中

if (node.waitStatus == Node.CONDITION || node.prev == null) // 如果waitStatus为CONDITION 或 node没有前驱节点,则必然不在同步队列,直接返回false

return false;

if (node.next != null) // If has successor, it must be on queue 如果有后继节点,必然是在同步队列中,返回true

return true;

/*

  • node.prev can be non-null, but not yet on queue because

  • the CAS to place it on queue can fail. So we have to

  • traverse from tail to make sure it actually made it. It

  • will always be near the tail in calls to this method, and

  • unless the CAS failed (which is unlikely), it will be

  • there, so we hardly ever traverse much.

*/

return findNodeFromTail(node); // 返回node是否为同步队列节点,如果是返回true,否则返回false

}

返回node是否为同步队列节点,如果是返回true,否则返回false。因为只有该节点的线程被唤醒(signal())才会从条件队列移到同步队列。

findNodeFromTail方法


private boolean findNodeFromTail(Node node) { // 从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false

Node t = tail;

for (;😉 {

if (t == node)

return true;

if (t == null)

return false;

t = t.prev;

}

}

从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false。

signal方法

========

public final void signal() {

if (!isHeldExclusively()) // 检查当前线程是否为独占模式同步器的所有者

throw new IllegalMonitorStateException();

Node first = firstWaiter;

if (first != null)

doSignal(first); // 唤醒条件队列的头节点

}

  1. 检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException。

  2. 拿到条件队列的头节点,如果不为null,则调用doSignal方法(详解见下文doSignal方法)唤醒头节点。

doSignal方法


private void doSignal(Node first) { // 将条件队列的头节点移到同步队列

do {

if ( (firstWaiter = first.nextWaiter) == null) // 将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null

lastWaiter = null;

first.nextWaiter = null; // 断开first节点对first节点后继节点的关联

} while (!transferForSignal(first) && // transferForSignal:将first节点从条件队列移动到同步队列

(first = firstWaiter) != null); // 如果transferForSignal失败,并且first节点不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 firstWaiter为null

}

  1. 将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null。

  2. 断开first节点与first节点后继节点的关联。

  3. 调用transferForSignal方法(详解见下文transferForSignal方法)将first节点从条件队列移动到同步队列。

  4. 如果transferForSignal失败,并且first节点的后继节点(firstWaiter)不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 first节点的后继节点为null。

transferForSignal方法


final boolean transferForSignal(Node node) { // 将node节点从条件队列移动到同步队列,如果成功则返回true。

/*

  • If cannot change waitStatus, the node has been cancelled.

*/

if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 如果不能更改节点的waitStatus,则表示该节点已被取消,返回false

return false;

/*

  • Splice onto queue and try to set waitStatus of predecessor to

  • indicate that thread is (probably) waiting. If cancelled or

  • attempt to set waitStatus fails, wake up to resync (in which

  • case the waitStatus can be transiently and harmlessly wrong).

*/

Node p = enq(node); // 否则,调用enq方法将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点

int ws = p.waitStatus; // 将ws赋值为node前驱节点的等待状态

if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // 如果node前驱节点的状态为CANCELLED(ws>0) 或 使用CAS将waitStatus修改成SIGNAL失败,则代表node的前驱节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点

LockSupport.unpark(node.thread);

return true;

}

  1. 如果不能更改节点的waitStatus,则表示该节点已被取消,返回false。

  2. 调用enq方法(详解见enq方法详解)将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点。因此,此时p节点为node的前驱节点。

  3. 将ws赋值为node前驱节点(p节点)的waitStatus。

  4. 如果p节点的waitStatus为CANCELLED(ws>0) 或 使用CAS将p节点的waitStatus修改成SIGNAL失败,则代表p节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点。

signalAll方法

===========

public final void signalAll() {

if (!isHeldExclusively()) // 检查当前线程是否为独占模式同步器的所有者

throw new IllegalMonitorStateException();

Node first = firstWaiter;

if (first != null)

doSignalAll(first); // 唤醒条件队列的所有节点

}

  1. 检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException。

  2. 拿到条件队列的头节点,如果不为null,则调用doSignalAll方法(详解见下文doSignalAll方法)唤醒条件队列的所有节点。

doSignalAll方法


private void doSignalAll(Node first) { // 将条件队列的所有节点移到同步队列

lastWaiter = firstWaiter = null; // 因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null

do {

Node next = first.nextWaiter; // next赋值为first节点的后继节点

first.nextWaiter = null; // 断开first节点对first节点后继节点的关联

transferForSignal(first); // transferForSignal:将first节点从条件队列移动到同步队列

first = next; // first赋值为next节点

} while (first != null); // 循环遍历,将条件队列的所有节点移动到同步队列

}

  1. 因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null。

  2. next赋值为first节点的后继节点 。

  3. 断开first节点对first节点后继节点的关联

  4. 调用transferForSignal方法(详解见上文transferForSignal方法)将first节点从条件队列移动到同步队列。

  5. first赋值为next节点,准备下一次循环。

  6. 如果first不为null,则进入下一次循环。

总结

==

  1. 调用await和signal方法都需要先获得锁,否则会抛异常。

最后

image.png

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!
ForSignal:将first节点从条件队列移动到同步队列

first = next; // first赋值为next节点

} while (first != null); // 循环遍历,将条件队列的所有节点移动到同步队列

}

  1. 因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null。

  2. next赋值为first节点的后继节点 。

  3. 断开first节点对first节点后继节点的关联

  4. 调用transferForSignal方法(详解见上文transferForSignal方法)将first节点从条件队列移动到同步队列。

  5. first赋值为next节点,准备下一次循环。

  6. 如果first不为null,则进入下一次循环。

总结

==

  1. 调用await和signal方法都需要先获得锁,否则会抛异常。

最后

[外链图片转存中…(img-xlfe49WZ-1714686933046)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值