Java-AQS-Condition原理及解析

AQS-Condition原理及解析

Condition基础概念

Condition可以理解为配合Lock使用的条件队列机制。当线程拿到锁之后,如果发现某个条件暂时不满足,就可以先进入等待;等到其他线程把条件修改完成,再显式地唤醒这些等待线程。

它解决的核心问题不是“加锁”本身,而是线程在持锁访问共享资源时,如何围绕某个条件进行等待和通知

synchronized体系里,线程协作主要依赖wait()/notify()/notifyAll();而在Lock体系里,对应的能力主要由Condition提供。

Condition最重要的特点是:**一个Lock可以创建多个Condition对象。**这意味着同一把锁下,可以把“等待队列”拆成多组,分别管理不同的等待条件,而不是像synchronized那样所有等待线程都挤在同一个隐含监视器队列里。

这里还要特别强调一点:Condition本身并不负责定义“条件是什么”。真正的业务条件仍然要靠共享状态变量自己表达,例如readycountqueue.isEmpty()Condition负责的只是“当条件不满足时如何等待、条件变化后如何通知”。

Condition与wait/notify的关系

如果把两套体系做一个简单类比,会更容易理解:

  • Condition.await() 类似于 Object.wait()
  • Condition.signal() 类似于 Object.notify()
  • Condition.signalAll() 类似于 Object.notifyAll()

它们在使用约束上也有明显的共性:

  • 线程必须先持有对应的锁,才能调用这些等待/通知方法
  • 调用等待方法后,当前线程会释放锁并进入等待状态
  • 被唤醒后,并不是立刻继续向下执行,而是还需要重新竞争锁

不过Condition相比Object.wait/notify更灵活:

  • Object监视器天然只有一个等待队列
  • Condition允许一个Lock下挂多个条件队列

例如在生产者-消费者模型里,可以把“队列非空”和“队列未满”拆成两个不同的Condition,分别唤醒真正需要被唤醒的线程,而不是每次都唤醒一整批线程再让它们重新竞争。

Condition基本使用

Condition的基本使用模式通常是固定的:

  1. 先获取锁
  2. 判断条件是否满足
  3. 条件不满足则调用await()
  4. 条件满足后执行业务逻辑
  5. 条件被改变后调用signal()signalAll()
  6. 最终在finally中释放锁

一个最常见的写法如下:

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
public class ConditionDemo {

private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;

public void awaitReady() throws InterruptedException {
lock.lock();
try {
while (!ready) {
condition.await();
}
System.out.println("条件满足,继续执行");
} finally {
lock.unlock();
}
}

public void signalReady() {
lock.lock();
try {
ready = true;
condition.signalAll();
} finally {
lock.unlock();
}
}
}

除了最基础的await()以外,Condition还提供了几种常见等待变体:

  • awaitUninterruptibly():不可中断等待
  • await(long time, TimeUnit unit):按相对时间等待
  • awaitNanos(long nanosTimeout):按纳秒等待,并返回剩余时间
  • awaitUntil(Date deadline):按绝对时间点等待

这些方法的核心等待逻辑是一致的,区别主要在于:是否响应中断、是否带超时、超时结果如何返回。

这里有几个容易忽略的点:

  • await()signal()/signalAll()都必须在持锁状态下调用,否则会抛出IllegalMonitorStateException
  • 判断条件时通常建议使用while而不是if
  • signal()只是通知某个等待线程有机会去继续竞争锁,不代表它马上就能执行
  • 正确顺序通常是:先修改共享状态,再发出signal()通知

之所以推荐用while,是因为线程被唤醒后,条件可能仍然不满足。比如多个线程都在等同一个条件,或者线程虽然被唤醒了,但在真正拿回锁时条件已经被其他线程改掉了。

再进一步说,使用while而不是if通常是为了同时覆盖下面几种情况:

  • 可能出现虚假唤醒,也就是线程即使没有等到明确的业务条件满足,也可能从等待中返回。
  • signalAll()会把多个等待线程都转移出来,但最终并不是每个线程都能在重新拿到锁时满足条件。
  • 线程被唤醒到真正重新获取锁之间,还存在一个竞争窗口,条件可能已经再次变化。

而“先改状态,再signal”这一点也非常关键。因为等待线程真正依赖的不是某一次通知动作本身,而是共享条件已经变为满足。如果先signal再改状态,就可能出现等待线程被唤醒后重新检查条件仍然失败,结果又回去继续等待,甚至形成看起来像“信号丢了”的问题。

Condition底层原理

ConditionAQS中的具体实现通常是ConditionObject。理解它时,最关键的是分清楚两条队列

  • 同步队列:AQS用来管理锁竞争的主队列
  • 条件队列:Condition自己维护的等待队列

线程调用await()时,并不是简单地“挂起”一下,而是会经历这样一个过程:

  1. 当前线程必须先持有锁
  2. 当前线程把自己包装成条件节点,加入Condition对应的条件队列
  3. 保存并释放当前持有的同步状态
  4. 进入等待,暂时不再参与锁竞争

这里的“释放当前持有的锁状态”如果放到ReentrantLock场景里,要理解得更完整一些:线程释放的并不是“只解一次锁”,而是会把自己当前持有的完整同步状态先保存下来,再彻底释放。这样其他线程才有机会真正获得这把锁;后面线程被唤醒并重新竞争成功后,还需要把之前保存的状态恢复回来。

而当其他线程调用signal()时,也不是直接让等待线程立刻恢复运行,而是:

  1. 从条件队列里取出一个等待节点
  2. 把这个节点转移到AQS的同步队列中
  3. 让它重新获得参与锁竞争的资格

因此,条件队列里的节点和同步队列里的节点虽然都与线程等待有关,但语义并不一样:

  • 条件队列表示“条件暂时不满足,所以先等待条件变化”
  • 同步队列表示“已经具备抢锁资格,正在等待重新获取锁”

这也是为什么说Condition并不是消息队列:它不会替你缓存“通知事件”本身。真正可靠的做法永远是:

  • 用共享状态表达条件
  • while循环检查条件
  • 在条件变化后再发出通知

离开了这三件事,只靠单独一次signal()并不能构成可靠的线程协作协议。

也就是说,Condition等待的线程其实会在条件队列同步队列之间流转:

  • await():从“正在执行”转为“进入条件队列等待”
  • signal():从“条件队列”转为“同步队列中等待重新抢锁”

这也是为什么说:signal唤醒,不等于已经拿到了锁。

await与signal完整流程

await -> signal -> 继续执行这条主线串起来,大致是下面这个过程:

1. 线程持有锁并检查条件

线程先通过lock.lock()拿到锁,然后检查当前条件是否满足。如果条件已经满足,线程就直接继续执行;如果条件不满足,才会进入等待。

2. 调用await进入等待

线程调用await()之后,会先把当前持有的锁释放掉。只有把锁释放掉,其他线程才有机会进入临界区去修改共享状态,否则条件永远都没有机会变化。

释放锁后,当前线程会进入Condition的条件队列,进入等待状态。

3. 其他线程修改条件并调用signal

另一个线程拿到同一把锁后,修改共享状态,让条件变为满足,然后调用signal()signalAll()

这一步的本质不是“让线程继续跑”,而是把等待线程从条件队列转移到同步队列

这里signal()signalAll()的区别也值得单独说明:

  • signal()只转移一个等待节点,适合一次条件变化只需要唤醒一个线程的场景。
  • signalAll()会把当前条件队列里的节点都转移出去,适合条件变化可能影响整批等待线程,或者难以精确判断该唤醒哪个线程的场景。

如果使用不当,就可能出现两类问题:

  • 该唤醒多个线程时只用了signal(),导致部分线程长时间等不到机会
  • 明明只需要唤醒一个线程,却频繁使用signalAll(),导致大量无效唤醒和锁竞争

4. 被唤醒线程重新竞争锁

等待线程被转移到同步队列后,仍然需要像普通抢锁线程一样重新竞争锁。只有当前持锁线程真正释放锁之后,它才有机会重新获取锁。

5. 重新获得锁后从await返回

线程重新拿到锁之后,await()方法才会返回,随后线程继续执行后面的逻辑。

所以从执行语义上看,await()的返回点并不表示“刚被通知”,而表示“已经被通知,并且已经重新获得了锁”。

除了正常的signal/signalAll路径外,await()还有一条很重要的分支是中断与取消

  • await()本身是可中断的,线程在等待过程中被中断时,不会像什么都没发生一样继续等待。
  • 中断发生后,节点会走取消或转移的相关处理逻辑,最终以抛出InterruptedException或重新设置中断标记的方式结束等待。

如果使用的是awaitUninterruptibly(),则行为会不同:线程不会因为中断而提前结束等待,而是会继续等到被正常唤醒并重新获取锁之后再返回。也正因为如此,它适合那些“当前等待不能因为中断而半途结束”的特殊场景,但在普通业务代码里通常要更谨慎使用。

也正因为存在中断、取消和条件再次变化这些情况,Condition的正确使用通常都离不开下面这组组合:

  • 持锁调用
  • while检查条件
  • 正确处理中断
  • 在合适时机选择signal()signalAll()

如果把它进一步上升成设计建议,可以记成:

  • 先定义共享状态,再定义等待条件
  • 永远围绕“条件是否成立”来写代码,而不是围绕“通知有没有发出”来写代码
  • 多个等待原因尽量拆成多个Condition,减少无效唤醒

小结

可以把Condition理解成:基于Lock + AQS实现的、更细粒度的等待/通知机制

相比Object.wait/notify,它最大的优势在于:

  • 一个锁可以绑定多个条件队列
  • 等待和唤醒逻辑更清晰
  • 更适合表达复杂并发场景下的条件协作

理解这篇的关键,不是记住多少方法名,而是抓住下面这条主线:

  • await():释放锁,进入条件队列等待
  • signal():把节点转移回同步队列
  • 被唤醒线程:重新竞争锁
  • 真正继续执行:发生在重新拿到锁之后

Java-AQS-Condition原理及解析
https://leo-wxy.github.io/2020/10/08/Java-AQS-Condition原理及解析/
作者
Leo-Wxy
发布于
2020年10月8日
许可协议