理解这两个队列是掌握 ReentrantLock
乃至整个 Java 并发包(JUC)中锁机制的关键。这两个队列都由 ReentrantLock
的内部工具类 AbstractQueuedSynchronizer
(简称 AQS) 来管理。
简单来说,这两个队列是:
- 同步队列 (Sync Queue):当线程尝试获取锁失败时,进入此队列等待。它是一个标准的“锁竞争”等待队列。
- 条件队列 (Condition Queue):当线程调用了
condition.await()
方法后,进入此队列等待。它是一个“条件”等待队列,用于线程间的通信与协作。
下面我们来深入剖析这两个队列。
1. 同步队列 (Sync Queue)
同步队列是 AQS 的核心组成部分,ReentrantLock
直接利用它来实现线程的排队和唤醒。
特点:
- 数据结构:一个 FIFO(先进先出)的双向链表。每个节点(
AQS.Node
)都包含了等待锁的线程引用、前驱节点、后继节点等信息。 - 作用:管理那些尝试获取锁(调用
lock()
方法)但失败的线程。 - 入队时机:当一个线程调用
lock()
方法时,如果锁已被其他线程持有,AQS 会为该线程创建一个Node
节点,并将其加入到同步队列的队尾。 - 出队时机:
- 当持有锁的线程调用
unlock()
方法释放锁时,AQS 会唤醒同步队列的队头节点(Head Node)的后继节点,让它去尝试获取锁。 - 如果成功获取锁,该节点将成为新的队头节点,旧的队头节点出队。
- 当持有锁的线程调用
- 线程状态:进入同步队列的线程会被
LockSupport.park()
方法挂起,进入休眠状态,不消耗 CPU 资源。直到被前一个节点释放锁后唤醒(unpark
)。
工作流程(以公平锁为例):
- 线程 A 调用
lock()
,成功获取锁,成为锁的持有者。 - 线程 B 调用
lock()
,发现锁被线程 A 持有,获取失败。 - AQS 为线程 B 创建一个
Node
,将其加入同步队列的尾部,并通过LockSupport.park(this)
将线程 B 挂起。 - 线程 C 调用
lock()
,同样获取失败,AQS 为其创建Node
并加入队列尾部(在 B 之后),然后挂起线程 C。 - 线程 A 调用
unlock()
释放锁。 - AQS 唤醒同步队列中队头节点的后继节点,也就是线程 B (
LockSupport.unpark(threadB)
)。 - 线程 B 被唤醒,从
park
处继续执行,成功获取锁。 - 当线程 B 释放锁时,再唤醒线程 C,以此类推。
2. 条件队列 (Condition Queue)
条件队列与 Condition
对象绑定,一个 ReentrantLock
可以通过 lock.newCondition()
创建多个 Condition
对象,从而拥有多个条件队列。这与 synchronized
只有一个等待队列(由 wait/notify
控制)形成鲜明对比。
特点:
- 数据结构:一个单向链表。它只负责收集调用了
await()
方法的线程,结构比同步队列简单。 - 作用:实现线程间的协作,类似于
Object.wait()
。 - 入队时机:当一个已经持有锁的线程调用了某个
condition.await()
方法时:- 该线程会释放它持有的锁。
- AQS 为该线程创建一个
Node
,并将其加入到条件队列的尾部。 - 线程被挂起等待。
- 出队(转移)时机:当另一个持有锁的线程调用了同一个
condition
对象的signal()
或signalAll()
方法时:signal()
:将条件队列的队头节点从条件队列中移动到同步队列的队尾。signalAll()
:将条件队列中的所有节点依次移动到同步队列的队尾。
- 重要:被
signal()
的线程并不会立即被唤醒执行。它只是从“等待条件”的状态变成了“等待锁”的状态,需要和同步队列中的其他线程一样,排队竞争锁。只有当它成功获取锁之后,才能从await()
方法中返回,继续执行。
工作流程:
- 线程 A 获取了锁,但发现某个条件不满足(例如,队列是空的),于是调用
condition.await()
。 - 线程 A 释放锁,并被放入条件队列中等待。
- 线程 B 获取了锁,执行操作,使得条件满足了(例如,向队列中添加了元素)。
- 线程 B 调用
condition.signal()
。 - AQS 将线程 A 所在的节点从条件队列转移到同步队列的队尾。此时线程 A 仍然是挂起状态。
- 线程 B 调用
unlock()
释放锁。 - AQS 按照同步队列的规则,唤醒队头的线程(可能是线程 A,也可能是其他更早进入同步队列的线程)。
- 轮到线程 A 时,它被唤醒,重新获取锁,然后从
await()
方法中返回,继续执行。
总结与对比
特性 | 同步队列 (Sync Queue) | 条件队列 (Condition Queue) |
---|---|---|
目的 | 竞争和等待锁 | 等待某个条件满足 (线程协作) |
数据结构 | FIFO 双向链表 | FIFO 单向链表 |
触发条件 | 线程调用 lock() 获取锁失败 |
持有锁的线程调用 condition.await() |
唤醒方式 | 前一个节点释放锁 (unlock() ) 时,唤醒后继节点 |
其他线程调用 condition.signal() / signalAll() |
唤醒后动作 | 尝试获取锁,成功后继续执行 | 从条件队列转移到同步队列,等待重新获取锁 |
与锁的关系 | 队列中的线程不持有锁,正在等待获取锁 | 入队时释放锁,等待被 signal 后再重新去竞争锁 |
关联对象 | 与 ReentrantLock 实例(AQS)直接关联,只有一个 |
与 Condition 对象关联,一个 Lock 可以有多个 Condition |
生动的比喻
把这两个队列想象成去银行办业务:
- 锁 (Lock):银行的业务窗口。
- 同步队列 (Sync Queue):在大厅里排队等待叫号的人群。来办业务(
lock()
)的人,如果窗口都有人,就得去排队。 - 条件队列 (Condition Queue):VIP 休息室。假设你需要办理一个特殊业务,需要等经理批准。你先在窗口(获取了锁),但柜员告诉你经理现在没空(条件不满足)。于是你让出窗口(
await()
释放锁),去 VIP 休息室等待(进入条件队列)。 signal()
:经理有空了,通知你去重新排队(从条件队列转移到同步队列),等待叫号去窗口办理业务。你并不能直接插队到窗口。unlock()
:一个窗口的业务办完了,柜员按下叫号器,让大厅排在最前面的人过来办理业务。