理解这两个队列是掌握 ReentrantLock 乃至整个 Java 并发包(JUC)中锁机制的关键。这两个队列都由 ReentrantLock 的内部工具类 AbstractQueuedSynchronizer (简称 AQS) 来管理。

简单来说,这两个队列是:

  1. 同步队列 (Sync Queue):当线程尝试获取锁失败时,进入此队列等待。它是一个标准的“锁竞争”等待队列。
  2. 条件队列 (Condition Queue):当线程调用了 condition.await() 方法后,进入此队列等待。它是一个“条件”等待队列,用于线程间的通信与协作。

下面我们来深入剖析这两个队列。


1. 同步队列 (Sync Queue)

同步队列是 AQS 的核心组成部分,ReentrantLock 直接利用它来实现线程的排队和唤醒。

特点:

  • 数据结构:一个 FIFO(先进先出)的双向链表。每个节点(AQS.Node)都包含了等待锁的线程引用、前驱节点、后继节点等信息。
  • 作用:管理那些尝试获取锁(调用 lock() 方法)但失败的线程。
  • 入队时机:当一个线程调用 lock() 方法时,如果锁已被其他线程持有,AQS 会为该线程创建一个 Node 节点,并将其加入到同步队列的队尾。
  • 出队时机
    • 当持有锁的线程调用 unlock() 方法释放锁时,AQS 会唤醒同步队列的队头节点(Head Node)的后继节点,让它去尝试获取锁。
    • 如果成功获取锁,该节点将成为新的队头节点,旧的队头节点出队。
  • 线程状态:进入同步队列的线程会被 LockSupport.park() 方法挂起,进入休眠状态,不消耗 CPU 资源。直到被前一个节点释放锁后唤醒(unpark)。

工作流程(以公平锁为例):

  1. 线程 A 调用 lock(),成功获取锁,成为锁的持有者。
  2. 线程 B 调用 lock(),发现锁被线程 A 持有,获取失败。
  3. AQS 为线程 B 创建一个 Node,将其加入同步队列的尾部,并通过 LockSupport.park(this) 将线程 B 挂起。
  4. 线程 C 调用 lock(),同样获取失败,AQS 为其创建 Node 并加入队列尾部(在 B 之后),然后挂起线程 C。
  5. 线程 A 调用 unlock() 释放锁。
  6. AQS 唤醒同步队列中队头节点的后继节点,也就是线程 B (LockSupport.unpark(threadB))。
  7. 线程 B 被唤醒,从 park 处继续执行,成功获取锁。
  8. 当线程 B 释放锁时,再唤醒线程 C,以此类推。

2. 条件队列 (Condition Queue)

条件队列与 Condition 对象绑定,一个 ReentrantLock 可以通过 lock.newCondition() 创建多个 Condition 对象,从而拥有多个条件队列。这与 synchronized 只有一个等待队列(由 wait/notify 控制)形成鲜明对比。

特点:

  • 数据结构:一个单向链表。它只负责收集调用了 await() 方法的线程,结构比同步队列简单。
  • 作用:实现线程间的协作,类似于 Object.wait()
  • 入队时机:当一个已经持有锁的线程调用了某个 condition.await() 方法时:
    1. 该线程会释放它持有的锁
    2. AQS 为该线程创建一个 Node,并将其加入到条件队列的尾部。
    3. 线程被挂起等待。
  • 出队(转移)时机:当另一个持有锁的线程调用了同一个 condition 对象的 signal()signalAll() 方法时:
    • signal():将条件队列的队头节点从条件队列中移动同步队列的队尾。
    • signalAll():将条件队列中的所有节点依次移动同步队列的队尾。
  • 重要:被 signal() 的线程并不会立即被唤醒执行。它只是从“等待条件”的状态变成了“等待锁”的状态,需要和同步队列中的其他线程一样,排队竞争锁。只有当它成功获取锁之后,才能从 await() 方法中返回,继续执行。

工作流程:

  1. 线程 A 获取了锁,但发现某个条件不满足(例如,队列是空的),于是调用 condition.await()
  2. 线程 A 释放锁,并被放入条件队列中等待。
  3. 线程 B 获取了锁,执行操作,使得条件满足了(例如,向队列中添加了元素)。
  4. 线程 B 调用 condition.signal()
  5. AQS 将线程 A 所在的节点从条件队列转移到同步队列的队尾。此时线程 A 仍然是挂起状态。
  6. 线程 B 调用 unlock() 释放锁。
  7. AQS 按照同步队列的规则,唤醒队头的线程(可能是线程 A,也可能是其他更早进入同步队列的线程)。
  8. 轮到线程 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():一个窗口的业务办完了,柜员按下叫号器,让大厅排在最前面的人过来办理业务。