Java-AQS-Condition原理及解析
AQS-Condition原理及解析
Condition基础概念
Condition可以理解为配合Lock使用的条件队列机制。当线程拿到锁之后,如果发现某个条件暂时不满足,就可以先进入等待;等到其他线程把条件修改完成,再显式地唤醒这些等待线程。
它解决的核心问题不是“加锁”本身,而是线程在持锁访问共享资源时,如何围绕某个条件进行等待和通知。
在synchronized体系里,线程协作主要依赖wait()/notify()/notifyAll();而在Lock体系里,对应的能力主要由Condition提供。
Condition最重要的特点是:**一个Lock可以创建多个Condition对象。**这意味着同一把锁下,可以把“等待队列”拆成多组,分别管理不同的等待条件,而不是像synchronized那样所有等待线程都挤在同一个隐含监视器队列里。
这里还要特别强调一点:Condition本身并不负责定义“条件是什么”。真正的业务条件仍然要靠共享状态变量自己表达,例如ready、count、queue.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的基本使用模式通常是固定的:
- 先获取锁
- 判断条件是否满足
- 条件不满足则调用
await() - 条件满足后执行业务逻辑
- 条件被改变后调用
signal()或signalAll() - 最终在
finally中释放锁
一个最常见的写法如下:
1 | |
除了最基础的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底层原理
Condition在AQS中的具体实现通常是ConditionObject。理解它时,最关键的是分清楚两条队列:
- 同步队列:AQS用来管理锁竞争的主队列
- 条件队列:Condition自己维护的等待队列
线程调用await()时,并不是简单地“挂起”一下,而是会经历这样一个过程:
- 当前线程必须先持有锁
- 当前线程把自己包装成条件节点,加入
Condition对应的条件队列 - 保存并释放当前持有的同步状态
- 进入等待,暂时不再参与锁竞争
这里的“释放当前持有的锁状态”如果放到ReentrantLock场景里,要理解得更完整一些:线程释放的并不是“只解一次锁”,而是会把自己当前持有的完整同步状态先保存下来,再彻底释放。这样其他线程才有机会真正获得这把锁;后面线程被唤醒并重新竞争成功后,还需要把之前保存的状态恢复回来。
而当其他线程调用signal()时,也不是直接让等待线程立刻恢复运行,而是:
- 从条件队列里取出一个等待节点
- 把这个节点转移到AQS的同步队列中
- 让它重新获得参与锁竞争的资格
因此,条件队列里的节点和同步队列里的节点虽然都与线程等待有关,但语义并不一样:
- 条件队列表示“条件暂时不满足,所以先等待条件变化”
- 同步队列表示“已经具备抢锁资格,正在等待重新获取锁”
这也是为什么说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():把节点转移回同步队列- 被唤醒线程:重新竞争锁
- 真正继续执行:发生在重新拿到锁之后