

在 Java 并发编程中,“线程通信” 是绕不开的核心话题 —— 当你需要让线程 A 等待线程 B 完成某个操作,或让生产者线程和消费者线程有序交互时,光靠锁保证线程安全还不够,必须通过通信机制协调线程执行节奏。今天这篇文章,我们从经典的 wait ()/notify () 讲到灵活的 Condition 接口,结合底层原理、实战代码和避坑指南,帮你彻底掌握线程通信的精髓。文中还配了多幅 SVG 插图,让抽象概念更直观,建议收藏慢慢看!
一、基础篇:Object 类的 “三剑客”——wait ()/notify ()/notifyAll ()
你可能疑惑:为什么 wait ()、notify () 这些线程通信方法,会定义在最顶层的 Object 类里?答案藏在 Java 的 “对象监视器”(Object Monitor)机制里 ——每个 Java 对象都自带一个监视器锁,而线程通信本质是基于监视器锁的等待与唤醒,所以这些方法必须属于 Object,而非 Thread。
1.1 核心规则:必须在 synchronized 块中调用
这是最容易踩的第一个坑!如果在没有持有对象锁的情况下调用 wait ()/notify (),JVM 会直接抛出IllegalMonitorStateException。原因很简单:
- 线程调用 wait () 时,需要先释放当前持有的监视器锁,让其他线程有机会获取锁;
- 线程调用 notify () 时,需要确保唤醒的是 “等待当前对象锁” 的线程,否则唤醒毫无意义。
只有在 synchronized 块(或 synchronized 方法)中,线程才持有对象的监视器锁,这是调用通信方法的前提。
1.2 三大方法的底层逻辑
我们用 “会议室场景” 类比:监视器锁是会议室钥匙,线程是参会者,等待队列是走廊候场区。
- wait():当前线程交出钥匙(释放锁),走进走廊候场(进入对象的等待队列),放弃 CPU 执行权,直到被唤醒;
- notify():当前线程从走廊里随机叫一个候场者(唤醒等待队列中的一个线程),让它重新排队抢钥匙(进入同步队列);
- notifyAll():当前线程叫出走廊里所有候场者(唤醒等待队列中的所有线程),让它们一起排队抢钥匙。
下面这幅图,清晰展示了线程调用这些方法后的状态流转:

1.3 入门实战:两个线程交替打印 1-100
用 wait ()/notify () 实现 “线程 A 打印奇数,线程 B 打印偶数”,核心是让线程打印后唤醒对方,自己进入等待:
public class WaitNotifyDemo {
// 共享计数器
private int count = 1;
// 共享锁对象(任意Object均可,只要线程间共享)
private final Object lock = new Object();
public static void main(String[] args) {
WaitNotifyDemo demo = new WaitNotifyDemo();
// 线程A:打印奇数
new Thread(() -> demo.printOdd(), "OddThread").start();
// 线程B:打印偶数
new Thread(() -> demo.printEven(), "EvenThread").start();
}
// 打印奇数(1,3,5...)
private void printOdd() {
while (count <= 100) {
synchronized (lock) {
// 若当前是奇数,打印并自增
if (count % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ": " + count);
count++;
// 打印后唤醒等待的偶数线程
lock.notify();
} else {
try {
// 若不是奇数,释放锁等待
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
// 打印偶数(2,4,6...)
private void printEven() {
while (count <= 100) {
synchronized (lock) {
// 若当前是偶数,打印并自增
if (count % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + count);
count++;
// 打印后唤醒等待的奇数线程
lock.notify();
} else {
try {
// 若不是偶数,释放锁等待
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}
}
运行代码会看到两个线程交替打印,这就是 wait ()/notify () 的核心作用:让线程在 “条件不满足” 时等待,“条件满足” 时唤醒。
二、进阶篇:Lock 锁的好搭档 ——Condition 接口
wait ()/notify () 虽然能解决基础通信问题,但有个明显缺陷:一个对象锁只能对应一个等待队列。比如生产者消费者模型中,若用 wait ()/notifyAll (),会同时唤醒生产者和消费者,导致无关线程被唤醒(“虚假唤醒” 的一种),增加 CPU 上下文切换开销。
这时就需要 Condition 接口 —— 它是 Java 1.5 中java.util.concurrent.locks包的产物,允许为一个 Lock 锁创建多个等待队列,实现 “精准唤醒”。
2.1 Condition 的核心用法
Condition 的使用必须结合 Lock 锁,步骤如下:
- 创建 Lock 对象(如 ReentrantLock);
- 通过
lock.newCondition()创建 Condition 实例(可创建多个); - 在
lock.lock()和lock.unlock()之间调用 Condition 的方法:- await():对应 wait (),释放锁并进入当前 Condition 的等待队列;
- signal():对应 notify (),唤醒当前 Condition 等待队列中的一个线程;
- signalAll():对应 notifyAll (),唤醒当前 Condition 等待队列中的所有线程。
下面这幅图展示了 Condition 的多队列优势:

2.2 进阶实战:精准唤醒的生产者消费者
用 Condition 实现生产者消费者模型,创建两个 Condition:
notFull:队列满时,生产者等待;notEmpty:队列空时,消费者等待。
这样生产者只会唤醒消费者,消费者只会唤醒生产者,避免无关线程唤醒:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionProducerConsumer {
// 队列容量
private static final int CAPACITY = 5;
// 存储数据的队列
private final Queue<Integer> queue = new LinkedList<>();
// Lock锁
private final Lock lock = new ReentrantLock();
// 两个Condition:队列不满、队列不空
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
// 数据计数器
private int data = 0;
public static void main(String[] args) {
ConditionProducerConsumer demo = new ConditionProducerConsumer();
// 启动3个生产者
for (int i = 0; i < 3; i++) {
new Thread(demo::produce, "Producer-" + i).start();
}
// 启动2个消费者
for (int i = 0; i < 2; i++) {
new Thread(demo::consume, "Consumer-" + i).start();
}
}
// 生产者:向队列添加数据
private void produce() {
while (true) {
lock.lock(); // 获取锁
try {
// 队列满时,生产者等待(释放锁)
while (queue.size() == CAPACITY) {
System.out.println(Thread.currentThread().getName() + ":队列满,等待消费者消费");
notFull.await(); // 进入notFull等待队列
}
// 生产数据并加入队列
data++;
queue.offer(data);
System.out.println(Thread.currentThread().getName() + ":生产数据" + data + ",队列大小:" + queue.size());
// 唤醒等待“队列不空”的消费者
notEmpty.signal();
// 模拟生产耗时
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
}
// 消费者:从队列获取数据
private void consume() {
while (true) {
lock.lock(); // 获取锁
try {
// 队列空时,消费者等待(释放锁)
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + ":队列空,等待生产者生产");
notEmpty.await(); // 进入notEmpty等待队列
}
// 消费数据
int consumedData = queue.poll();
System.out.println(Thread.currentThread().getName() + ":消费数据" + consumedData + ",队列大小:" + queue.size());
// 唤醒等待“队列不满”的生产者
notFull.signal();
// 模拟消费耗时
Thread.sleep(800);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 必须在finally中释放锁
}
}
}
}
运行代码会发现:生产者满时只等消费者,消费者空时只等生产者,没有无关唤醒 —— 这就是 Condition 的 “精准” 优势。
三、深度对比:wait ()/notify () vs Condition
为了帮你快速选型,我整理了两者的核心差异,用表格和图呈现:
| 对比维度 | wait()/notify() | Condition 接口 |
|---|---|---|
| 锁依赖 | 依赖 Object 类的监视器锁(synchronized) | 依赖 Lock 锁(如 ReentrantLock) |
| 等待队列数量 | 一个对象锁对应一个等待队列 | 一个 Lock 锁可对应多个等待队列 |
| 唤醒方式 | 只能唤醒随机线程(notify ())或所有线程(notifyAll ()) | 可唤醒指定队列的线程(signal ()/signalAll ()),支持精准唤醒 |
| 功能扩展 | 无额外功能,不支持中断超时 | 支持中断等待(awaitInterruptibly ())、超时等待(await (long, TimeUnit))、定时等待(awaitUntil (Date)) |
| 底层实现 | 基于 ObjectMonitor(JVM 底层 C++ 实现) | 基于 AQS 的 ConditionObject(Java 代码实现) |
| 异常处理 | 需手动捕获 InterruptedException | 部分方法(如 awaitInterruptibly ())允许抛出中断异常 |
下面是差异的可视化图,更直观:

四、避坑指南:90% 开发者会踩的 5 个坑
线程通信看似简单,但实际开发中很容易因细节疏忽导致 bug。结合我的项目经验,总结了 5 个高频坑:
坑 1:用 if 判断等待条件,忽略 “虚假唤醒”
错误写法:用 if 判断条件,线程被唤醒后直接执行,不重新检查条件。
synchronized (lock) {
if (queue.isEmpty()) { // 错误:if只判断一次
lock.wait();
}
// 消费数据
}
问题:线程可能被 “虚假唤醒”(比如其他线程误唤醒),此时队列仍为空,执行消费会抛异常。正确写法:用 while 循环,唤醒后重新检查条件:
synchronized (lock) {
while (queue.isEmpty()) { // 正确:while循环重新检查
lock.wait();
}
// 消费数据
}
坑 2:notify () 后未释放锁,唤醒线程无法执行
错误写法:notify () 后仍在执行耗时操作,未释放锁。
synchronized (lock) {
count++;
lock.notify(); // 唤醒后仍持有锁
try {
Thread.sleep(1000); // 耗时操作,锁未释放
} catch (InterruptedException e) {}
}
问题:被唤醒的线程进入同步队列后,因锁未释放,只能一直等待,导致 “唤醒无效”。正确写法:notify () 后尽快释放锁,避免后续耗时操作:
synchronized (lock) {
count++;
lock.notify(); // 唤醒后立即退出同步块,释放锁
}
// 耗时操作移到同步块外
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
坑 3:Condition 的 await () 未在 Lock 保护下调用
错误写法:未调用 lock.lock () 就调用 await ()。
Condition condition = lock.newCondition();
condition.await(); // 错误:未获取Lock锁
问题:抛出IllegalMonitorStateException,原因和 wait () 类似 ——Condition 依赖 Lock 锁,必须在锁保护下调用。正确写法:在 lock.lock () 和 lock.unlock () 之间调用:
lock.lock();
try {
condition.await(); // 正确:持有Lock锁
} finally {
lock.unlock();
}
坑 4:忘记在 finally 中释放 Lock 锁
错误写法:Lock 锁未在 finally 中释放,若中间抛异常,锁会泄漏。
lock.lock();
if (count > 0) {
throw new RuntimeException("异常"); // 抛异常后,lock.unlock()未执行
}
lock.unlock();
问题:锁一直被当前线程持有,其他线程无法获取,导致死锁。正确写法:必须在 finally 中释放 Lock 锁:
lock.lock();
try {
if (count > 0) {
throw new RuntimeException("异常");
}
} finally {
lock.unlock(); // 无论是否抛异常,都释放锁
}
坑 5:滥用 notifyAll () 导致性能损耗
错误场景:生产者消费者模型中,用 notifyAll () 同时唤醒生产者和消费者。问题:无关线程被唤醒后,检查条件不满足又会 wait (),增加 CPU 上下文切换开销。解决方案:用 Condition 的多队列精准唤醒,只唤醒相关线程(如生产者唤醒消费者,消费者唤醒生产者)。
五、总结:线程通信的核心思想
看到这里,你可能已经发现:无论是 wait ()/notify () 还是 Condition,线程通信的核心思想都是 “条件等待 + 精准唤醒”
- 线程在 “条件不满足” 时,释放锁并进入等待队列,避免忙等(空循环占 CPU);
- 线程在 “条件满足” 时,唤醒等待队列中需要的线程,让它们重新竞争锁执行。
简单场景下,wait ()/notify () 足够用;当需要精准唤醒、超时等待等复杂需求时,Condition 是更优选择。
最后,留一个思考题:如果让你用 Condition 实现 “三个线程交替打印 ABC”,你会怎么写?欢迎在评论区分享你的思路!
如果觉得这篇文章对你有帮助,别忘了点赞 + 收藏,关注我,后续还会更新更多 Java 并发深度干货~


1031

被折叠的 条评论
为什么被折叠?



