一、总览对比表

通信方式 核心组件 适用场景 是否释放锁 能否指定唤醒
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 线程通信的全部场景,掌握它们,多线程通信无忧。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐