Java 线程通信的 6 种核心方式及其关键细节
一、总览对比表
| 通信方式 | 核心组件 | 适用场景 | 是否释放锁 | 能否指定唤醒 |
|---|---|---|---|---|
| synchronized + wait/notify | 对象监视器 | 简单的一对一/多对多通信 | ✅ 释放 | ❌ 随机/全部 |
| Lock + Condition | 可重入锁 + 条件变量 | 复杂的多条件通信(如生产者-消费者) | ✅ 释放 | ✅ 精确指定 |
| volatile | 共享变量 | 简单的状态标志位 | ❌ 不涉及 | ❌ 不涉及 |
| Thread.join() | 线程实例 | 等待另一个线程结束 | ❌ 不涉及 | ❌ 不涉及 |
| CountDownLatch | 倒计时门闩 | 等待多个线程完成某项准备 | ❌ 不涉及 | ✅ 批量唤醒 |
| BlockingQueue | 阻塞队列 | 生产者-消费者模式(数据传递) | ❌ 不涉及 | ✅ 自动唤醒 |
二、每种方式的详细说明
1. synchronized + wait() / notify() / notifyAll()
核心原理:每个 Java 对象都有一个等待集和锁。
关键代码模板:
synchronized (lock) {
while (!condition) { // ⚠️ 必须用while,防止虚假唤醒
lock.wait(); // 释放锁,进入等待
}
// 条件满足,继续执行
lock.notify(); // 随机唤醒一个等待线程
// 或 lock.notifyAll(); // 唤醒所有等待线程
}
致命细节:
-
必须先获得锁:wait/notify 必须在 synchronized 代码块内调用,否则抛 IllegalMonitorStateException。
-
wait() 会释放锁:线程进入等待时,完全释放锁;被唤醒后,需要重新竞争锁才能继续。
-
notify() 不释放锁:调用 notify() 后,当前线程不会立即释放锁,而是执行完 synchronized 块后才释放。
-
必须用 while 循环:防止虚假唤醒(spurious wakeup),即线程没收到 notify 也被唤醒。
-
无法精确唤醒:notify() 随机唤醒一个,notifyAll() 唤醒全部,无法指定唤醒某个特定线程。
典型错误:
// ❌ 错误:用 if 判断条件
if (queue.isEmpty()) {
queue.wait(); // 可能被虚假唤醒,醒来后条件可能仍不满足
}
// ✅ 正确:用 while 循环
while (queue.isEmpty()) {
queue.wait();
}
2. Lock + Condition(推荐用于复杂场景)
核心原理:一个 Lock 可以创建多个 Condition 对象,每个 Condition 有独立的等待队列。
关键代码模板:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 条件1:队列未满
Condition notEmpty = lock.newCondition(); // 条件2:队列非空
// 生产者
lock.lock();
try {
while (isFull()) {
notFull.await(); // 等待队列未满
}
// 生产数据...
notEmpty.signal(); // 精确唤醒消费者
} finally {
lock.unlock(); // ⚠️ 必须finally解锁
}
// 消费者
lock.lock();
try {
while (isEmpty()) {
notEmpty.await(); // 等待队列非空
}
// 消费数据...
notFull.signal(); // 精确唤醒生产者
} finally {
lock.unlock();
}
核心优势:
-
✅ 精确唤醒:可以唤醒特定条件队列上的线程(如只唤醒生产者,不唤醒消费者)。
-
✅ 多个等待队列:比 synchronized 更灵活。
-
✅ 可响应中断:
await()可中断,也有awaitUninterruptibly()不可中断版本。
致命细节:
-
必须 try-finally 解锁:await() 会抛出 InterruptedException,必须确保 finally 中 unlock()。
-
await() 释放锁:与 wait() 类似,释放锁并进入等待;被 signal() 后重新竞争锁。
-
signal() 不释放锁:与 notify() 类似,调用后需要退出 lock 块才真正释放锁。
-
必须用 while 循环:同样防止虚假唤醒。
与 synchronized 对比:
| 特性 | synchronized | Lock + Condition |
|---|---|---|
| 精确唤醒 | ❌ 只能随机/全部 | ✅ 可精确唤醒指定队列 |
| 多个等待队列 | ❌ 只有一个 | ✅ 可以有多个 |
| 可中断等待 | ❌ 只能等待 | ✅ await() 可中断 |
| 超时等待 | ❌ 不支持 | ✅ await(time, unit) |
3. volatile 共享变量
核心原理:volatile 保证变量的可见性(一个线程修改后,其他线程立即看到)和有序性(禁止指令重排),但不保证原子性。
关键代码模板:
volatile boolean flag = false;
// 线程A:等待
while (!flag) {
// 自旋等待,不释放锁也不阻塞
Thread.onSpinWait(); // Java 9+ 提示CPU正在自旋
}
// flag变为true后继续
// 线程B:唤醒
flag = true; // 修改后,线程A立即可见
适用场景:
-
简单的状态标志(如停止标志、就绪标志)。
-
不适用于复杂的数据交换或需要阻塞等待的场景。
优缺点:
-
✅ 极轻量,无锁,无阻塞。
-
❌ 浪费CPU(自旋等待),不适合长时间等待。
-
❌ 只能一对一简单通信。
典型应用:线程中断标志、单例模式的双重检查锁。
4. Thread.join()
核心原理:当前线程等待目标线程彻底结束后才继续执行。
关键代码模板:
Thread t = new Thread(() -> {
// 执行任务
Thread.sleep(1000);
});
t.start();
t.join(); // 当前线程阻塞,直到t执行完毕
System.out.println("t已结束");
超时版本:
t.join(500); // 最多等待500ms,超时后不再等待
细节:
-
join() 的本质是调用 wait():查看 JDK 源码,
join()内部是synchronized (this) { while (isAlive()) { wait(0); } }。 -
必须捕获 InterruptedException。
-
只能等待线程结束,不能用于任意条件。
5. CountDownLatch(一次性门闩)
核心原理:计数器倒计时,当计数器减到 0 时,所有等待的线程被唤醒。
关键代码模板:
CountDownLatch latch = new CountDownLatch(3); // 等待3个事件
// 主线程:等待
latch.await(); // 阻塞,直到计数器变为0
// 3个工作线程:各执行一次
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 完成工作...
latch.countDown(); // 计数器减1
}).start();
}
关键细节:
-
不可重用:计数器归零后,不能再重置(若需重用用 CyclicBarrier)。
-
await() 可超时:
await(1, TimeUnit.SECONDS)。 -
countDown() 可多次调用:但不能减到负数。
-
唤醒时机:计数器变为 0 的瞬间,所有等待线程同时被唤醒。
适用场景:
-
主线程等待多个子线程完成初始化。
-
并发测试中,等待所有线程准备就绪。
6. BlockingQueue(阻塞队列)
核心原理:队列满时 put() 阻塞,队列空时 take() 阻塞,实现生产者-消费者模式。
关键代码模板:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
queue.put(data); // 队列满时阻塞
// 消费者
Integer data = queue.take(); // 队列空时阻塞
常用实现类:
| 实现类 | 特性 |
|---|---|
| ArrayBlockingQueue | 有界,基于数组,公平锁可选 |
| LinkedBlockingQueue | 可选有界/无界,基于链表 |
| SynchronousQueue | 无容量,每个 put 必须等待 take |
| PriorityBlockingQueue | 无界,按优先级排序 |
细节:
-
自动唤醒:
put()和take()内部自动处理等待和唤醒,无需手动编写条件判断。 -
高耦合封装:是生产者-消费者模式的最高级封装。
-
可中断:
put()和take()都响应中断。
三、核心细节总结
细节1:wait/await 释放锁,但 notify/signal 不释放锁
-
调用
wait()或await()时,线程立即释放锁,进入等待。 -
调用
notify()或signal()后,被唤醒的线程不会立即执行,因为当前线程仍持有锁。只有当前线程退出 synchronized/lock 块后,被唤醒的线程才能竞争锁。
细节2:必须用 while 循环判断条件
-
原因:防止虚假唤醒(spurious wakeup)。
-
虚假唤醒:线程在没有收到 notify/signal 的情况下被唤醒(JVM 实现允许)。
-
正确写法:
while (!condition) { wait(); }。
细节3:必须释放锁(finally 或 try-finally)
-
synchronized:JVM 保证异常时自动释放锁。
-
Lock:必须手动在 finally 中
unlock(),否则异常会导致死锁。
细节4:Condition 与 Lock 是一对一关系
-
一个 Lock 可以创建多个 Condition,每个 Condition 有独立的等待队列。
-
唤醒必须用同一个 Condition 对象:
conditionA.await()只能用conditionA.signal()唤醒。
细节5:高级工具避免手写 wait/notify
-
优先使用:BlockingQueue、CountDownLatch、CyclicBarrier、CompletableFuture。
-
原因:手写 wait/notify 容易出错(忘记 while 循环、异常未处理、虚假唤醒)。
四、选型决策树
需要线程通信?
│
├─ 只需要传递数据? → BlockingQueue
│
├─ 只需要等待线程结束? → Thread.join()
│
├─ 需要复杂的条件等待(多个条件队列)? → Lock + Condition
│
├─ 只需要简单的状态标志? → volatile
│
├─ 需要等待多个事件完成? → CountDownLatch / CyclicBarrier
│
└─ 简单的锁通信(单条件)? → synchronized + wait/notify
五、记忆口诀
synchronized 配 wait,while 循环防假醒。
Lock 配 Condition,try-finally 锁必停。
volatile 标志轻,自旋等待费 CPU。
join 等线程死,CountDown 等归零。
队列通信最省心,阻塞取放自动行。
这六种方式覆盖了 Java 线程通信的全部场景,掌握它们,多线程通信无忧。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)