在 Java 并发编程中,线程协作(Thread Cooperation) 是让多个线程有序执行、避免资源竞争的核心机制。很多开发者熟悉 wait()/notify(),但在面对复杂场景时,往往对 Condition 的 await()/signal() 以及底层的 LockSupport.park()/unpark() 感到困惑。
这三者究竟是什么关系?线程在等待时到底去了哪里?唤醒后又是如何重新获取锁的?本文将通过层层递进的方式,结合数据结构图解和源码逻辑,为你彻底梳理 Java 线程协作的底层脉络。
第一层:基础协作 —— Object 的 wait() / notify()
这是 Java 最原始的线程协作方式,基于 synchronized 关键字和对象监视器(Monitor)。
1. 核心机制:单队列模型
在 HotSpot JVM 中,每个对象头都关联一个 Monitor。Monitor 内部主要维护两个队列:
- Entry List(入口队列/阻塞队列):竞争锁失败的线程在此排队,状态为
BLOCKED。 - Wait Set(等待集合):调用
wait()的线程在此排队,状态为WAITING。
2. 线程流转过程
- 等待:线程调用
obj.wait(),释放锁,从 Owner 身份退出,进入 Wait Set。 - 通知:其他线程调用
obj.notify(),JVM 从 Wait Set 中随机选择一个线程。 - 转移:被选中的线程从 Wait Set 移动到 Entry List,状态变为
BLOCKED。 - 竞争:该线程必须与其他在 Entry List 中的线程重新竞争锁,只有获取锁后,
wait()方法才会返回。
痛点:由于只有一个 Wait Set,
notify()无法区分唤醒的是“生产者”还是“消费者”。为了安全,通常被迫使用notifyAll(),导致所有等待线程都被唤醒并竞争锁,造成“惊群效应”,性能低下。
第二层:精准协作 —— Lock 与 Condition
为了解决上述痛点,JUC 包提供了 ReentrantLock 和 Condition。这是现代 Java 并发编程的推荐方案。
1. 核心突破:多等待队列
一个 ReentrantLock 可以创建多个 Condition 对象。每个 Condition 维护一个独立的等待队列。
- 隔离性:生产者等待在
notFull队列,消费者等待在notEmpty队列。 - 精准唤醒:
signal()只唤醒特定队列的头节点,互不干扰。
2. 代码实战:生产者-消费者模型
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final LinkedList<Integer> buffer = new LinkedList<>();
private final int CAPACITY = 5;
private final ReentrantLock lock = new ReentrantLock();
// 创建两个独立的条件对象
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void produce(int item) throws InterruptedException {
lock.lock();
try {
// 缓冲区满,进入 notFull 队列等待
while (buffer.size() == CAPACITY) {
notFull.await();
}
buffer.add(item);
System.out.println("生产: " + item);
// 精准唤醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
// 缓冲区空,进入 notEmpty 队列等待
while (buffer.isEmpty()) {
notEmpty.await();
}
int item = buffer.removeFirst();
System.out.println("消费: " + item);
// 精准唤醒生产者
notFull.signal();
} finally {
lock.unlock();
}
}
}
第三层:底层揭秘 —— AQS 的双队列架构
Condition 的高效源于其底层 AQS (AbstractQueuedSynchronizer) 的精妙设计。理解线程在队列间的物理移动是掌握并发原理的关键。
1. 数据结构:Node 的复用与独立链表
AQS 并没有为 Condition 创建新的节点类,而是复用了内部的 Node 类,但维护了两条独立的链表:
| 特性 | AQS 同步队列 (Sync Queue) | Condition 等待队列 (Condition Queue) |
|---|---|---|
| 作用 | 竞争锁失败的线程排队 | 调用 await() 的线程排队 |
| 结构 | 双向链表 (prev/next) | 单向链表 (nextWaiter) |
| 头节点 | 有虚拟 Head 节点 | 无虚拟头,firstWaiter 指向首元素 |
| 状态标记 | waitStatus 为 0, SIGNAL, CANCELLED | waitStatus 强制为 CONDITION (-2) |
2. 线程流转图解:从 await 到 signal
线程并不是在两个队列间“瞬间跳跃”,而是经历了移除和添加的物理过程。
graph LR
subgraph Condition_Queue [Condition 等待队列]
C1[Node: CONDITION] --> C2[Node: CONDITION]
end
subgraph Sync_Queue [AQS 同步队列]
S_Head[Head] <--> S1[Node: SIGNAL] <--> S2[Node: 0]
end
C1 -- signal() 触发转移 --> S2
style C1 fill:#f9f,stroke:#333,stroke-width:2px
style S2 fill:#ccf,stroke:#333,stroke-width:2px
详细流转步骤:
- await():进入 Condition 队列
- 线程释放锁。
- 封装成 Node (
waitStatus = CONDITION)。 - 尾插入 Condition 队列。
- 调用
LockSupport.park()阻塞。
- signal():转移到同步队列
- 找到 Condition 队列的头节点。
- 关键动作:将该节点从 Condition 队列移除。
- 调用
enq(node)将该节点添加到 AQS 同步队列 尾部。 - 修改
waitStatus为0或SIGNAL。 - 注意:此时线程依然处于 park 阻塞状态,只是换了个队列排队。
- acquireQueued():竞争锁
- 当同步队列的前驱节点释放锁后,
unpark当前节点。 - 线程被唤醒,尝试获取锁。
- 获取成功后,成为 Sync Queue 的新 Head,
await()返回。
- 当同步队列的前驱节点释放锁后,
第四层:原子基石 —— LockSupport 的 park() / unpark()
无论是 wait/notify 还是 Condition,底层最终都依赖 LockSupport 来挂起和恢复线程。
1. 核心原理:许可(Permit)机制
park():如果 permit 为 0,线程阻塞;如果为 1,消费 permit 并立即返回。unpark(Thread t):将线程 t 的 permit 设为 1。如果 t 正在阻塞,则立即唤醒。
2. 为什么它是基石?
- 无需持锁:可以在任何地方调用,不像
wait必须在 synchronized 块中。 - 顺序无关:可以先
unpark后park,信号不会丢失。这使得它在实现复杂的非阻塞算法时非常灵活。 - 精准指向:
unpark必须指定具体的线程对象,天然支持精准唤醒。
总结与对比
| 维度 | wait / notify | Condition (await/signal) | LockSupport (park/unpark) |
|---|---|---|---|
| 层级 | 高级 API (JVM 内置) | 高级 API (JUC 框架) | 底层原语 (Unsafe) |
| 锁依赖 | 必须持有 synchronized 锁 | 必须持有 Lock 锁 | 无需任何锁 |
| 队列数量 | 每对象 1 个 Wait Set | 每 Lock N 个 Condition 队列 | 无队列概念,基于 Permit |
| 唤醒精度 | 随机或全部 | 精准指定条件队列 | 精准指定线程 |
| 适用场景 | 简单同步 | 复杂业务协作(推荐) | 编写底层并发工具 |
- 日常开发:优先使用
ReentrantLock+Condition,利用多队列实现高效、精准的线程协作。 - 源码阅读:重点关注
ConditionObject中await()和signal()如何将 Node 在两个队列间转移,这是理解 AQS 的钥匙。 - 底层理解:记住
park/unpark是基于 Permit 的原子操作,它是所有上层同步工具的“发动机”。
通过理解这四层结构,你不仅能写出更高效的并发代码,更能透过现象看本质,掌握 Java 并发编程的灵魂。
评论区