Java 多线程通信深度解析:从 wait/notify 到 Condition,手把手带你搞定线程协作

        在 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 锁,步骤如下:

  1. 创建 Lock 对象(如 ReentrantLock);
  2. 通过lock.newCondition()创建 Condition 实例(可创建多个);
  3. 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,线程通信的核心思想都是 “条件等待 + 精准唤醒

  1. 线程在 “条件不满足” 时,释放锁并进入等待队列,避免忙等(空循环占 CPU);
  2. 线程在 “条件满足” 时,唤醒等待队列中需要的线程,让它们重新竞争锁执行。

        简单场景下,wait ()/notify () 足够用;当需要精准唤醒、超时等待等复杂需求时,Condition 是更优选择。

        最后,留一个思考题:如果让你用 Condition 实现 “三个线程交替打印 ABC”,你会怎么写?欢迎在评论区分享你的思路!

        如果觉得这篇文章对你有帮助,别忘了点赞 + 收藏,关注我,后续还会更新更多 Java 并发深度干货~

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梵得儿SHI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值