在 Java 并发编程中,线程间的协同与通信是核心问题之一。当多个线程共同完成一项任务时,它们需要明确的“信号机制”来协调执行顺序、共享资源访问或等待特定条件达成。wait/notify(基于对象监视器)、CountDownLatch(倒计时门闩)、CyclicBarrier(循环屏障)是 Java 中实现线程通信的常用工具,但它们的设计理念、适用场景与使用方式存在显著差异。本文将从基础原理、核心特性、多维度对比及实战案例四个方面,带大家全面掌握这三种机制的精髓。
一、基础原理:从“是什么”开始
在深入对比前,我们首先需要明确每种机制的核心定义与工作原理——这是理解其差异的基础。
1.1 wait/notify:基于对象监视器的“原始信号”
wait/notify 是 Java 最基础的线程通信机制,源于 java.lang.Object 类(所有对象都具备此能力),其核心依赖**对象监视器(Monitor)**实现,与 synchronized 关键字紧密绑定。
-
wait():使当前线程释放对象的监视器锁,进入该对象的等待队列,直至被其他线程通过
notify()或notifyAll()唤醒,或等待超时。 -
notify():唤醒等待该对象监视器的单个线程(唤醒哪个线程由 JVM 随机决定),使其从等待队列进入锁竞争队列。
-
notifyAll():唤醒等待该对象监视器的所有线程,使其全部进入锁竞争队列。
核心特点:必须在 synchronized 代码块/方法中使用,否则会抛出 IllegalMonitorStateException——本质是确保线程操作对象监视器的合法性。
1.2 CountDownLatch:基于“倒计时”的一次性屏障
CountDownLatch 是 JUC(java.util.concurrent)包下的工具类,核心功能是让一个或多个线程等待其他线程完成指定操作后再继续执行,其底层依赖 AQS(AbstractQueuedSynchronizer)实现。
-
初始化:通过构造函数
CountDownLatch(int count)设置“倒计时总数”,代表需要等待的线程/任务数量。 -
countDown():当一个线程完成任务后调用此方法,倒计时总数减 1(线程安全,无需额外同步)。
-
await():需要等待的线程调用此方法后会阻塞,直至倒计时总数变为 0,或被中断。此外还有带超时参数的
await(long timeout, TimeUnit unit),避免无限阻塞。
核心特点:一次性使用,倒计时总数变为 0 后,CountDownLatch 就失去了作用,无法重置。
1.3 CyclicBarrier:基于“集合点”的可循环屏障
CyclicBarrier 同样是 JUC 包下的工具类,字面意思是“循环屏障”,核心功能是让一组线程到达某个“集合点”后阻塞,直至所有线程都到达该点,再一起继续执行,底层也依赖 AQS 实现。
-
初始化:通过构造函数
CyclicBarrier(int parties)设置“参与线程总数”,或CyclicBarrier(int parties, Runnable barrierAction)——后者指定一个“屏障动作”,当所有线程到达集合点后,会先执行该动作(由最后一个到达的线程执行)。 -
await():线程到达集合点后调用此方法,会阻塞自身,直至所有线程都调用了
await()。同样支持带超时参数的重载方法,超时后会抛出TimeoutException,且该线程会被标记为“中断”,屏障会被打破,其他等待线程也会被唤醒并抛出异常。 -
reset():重置 CyclicBarrier 到初始状态,可实现“循环使用”——这是其与 CountDownLatch 的核心区别之一。
核心特点:可循环复用,通过 reset() 方法重置后,能再次用于新的线程组任务;支持“屏障动作”,满足复杂场景需求。
二、核心差异:多维度对比分析
理解了基础原理后,我们从“设计目标、复用性、同步方向”等 8 个核心维度对比三种机制,明确其适用边界。
| 对比维度 | wait/notify | CountDownLatch | CyclicBarrier |
|---|---|---|---|
| 核心设计目标 | 线程间的“条件等待”与“信号通知”,实现灵活的协同(如生产者-消费者) | 单方向等待:1/N 线程等待 N/1 线程完成任务 | 双向等待:一组线程互相等待,直至全部到达集合点 |
| 依赖基础 | Object 类的监视器方法,与 synchronized 绑定 | JUC 的 AQS,基于共享锁实现 | JUC 的 AQS,基于独占锁实现 |
| 复用性 | 可重复使用,只要对象监视器有效 | 一次性,count 为 0 后无法重置 | 可循环使用,支持 reset() 重置 |
| 同步方向 | 双向:等待线程与通知线程可互相切换 | 单向:等待线程被动等待,计数线程主动减 1 | 双向:所有线程都是“等待者”与“唤醒者” |
| 异常处理 | 需手动处理中断,无超时重载(需结合 sleep 模拟) | 支持中断与超时,抛出 InterruptedException/TimeoutException | 支持中断与超时,抛出 InterruptedException/TimeoutException/BrokenBarrierException |
| 额外功能 | 无额外功能,仅提供基础通知机制 | 无额外功能,专注倒计时等待 | 支持“屏障动作”,所有线程到达后执行指定任务 |
| 使用复杂度 | 较高,需手动控制锁的获取与释放,避免死锁/假唤醒 | 较低,API 简洁,只需调用 countDown() 与 await() | 中等,需注意屏障被打破后的异常处理,支持 reset() 增强灵活性 |
| 典型场景 | 生产者-消费者模型、线程间条件触发 | 主线程等待子线程全部完成、任务拆分后的汇总 | 多线程并行计算(如分块处理数据后合并结果)、循环迭代任务 |
三、实战案例:从理论到实践
理论对比终显抽象,结合具体场景的实战案例,能更清晰地体现三种机制的应用差异。
3.1 wait/notify 实战:生产者-消费者模型
生产者-消费者模型是 wait/notify 的经典场景:生产者生成数据存入缓冲区,消费者从缓冲区取出数据,当缓冲区满时生产者等待,当缓冲区空时消费者等待。
import java.util.LinkedList;
import java.util.Queue;
// 缓冲区类(共享资源)
class Buffer {
private static final int MAX_CAPACITY = 5; // 缓冲区最大容量
private Queue<Integer> queue = new LinkedList<>();
// 生产者存入数据
public synchronized void put(int data) throws InterruptedException {
// 注意:用 while 而非 if,避免假唤醒
while (queue.size() == MAX_CAPACITY) {
System.out.println("缓冲区已满,生产者 " + Thread.currentThread().getName() + " 等待");
wait(); // 释放锁,进入等待队列
}
queue.offer(data);
System.out.println("生产者 " + Thread.currentThread().getName() + " 存入数据:" + data);
notifyAll(); // 唤醒所有等待线程(避免只唤醒同类线程)
}
// 消费者取出数据
public synchronized int take() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println("缓冲区为空,消费者 " + Thread.currentThread().getName() + " 等待");
wait();
}
int data = queue.poll();
System.out.println("消费者 " + Thread.currentThread().getName() + " 取出数据:" + data);
notifyAll();
return data;
}
}
// 测试类
public class ProducerConsumerWithWaitNotify {
public static void main(String[] args) {
Buffer buffer = new Buffer();
// 3 个生产者线程
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
for (int j = 1; j <= 4; j++) {
try {
buffer.put(j);
Thread.sleep(100); // 模拟生产耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "P" + i).start();
}
// 2 个消费者线程
for (int i = 1; i <= 2; i++) {
new Thread(() -> {
for (int j = 1; j <= 6; j++) {
try {
buffer.take();
Thread.sleep(200); // 模拟消费耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "C" + i).start();
}
}
}
关键注意点:必须用 while 循环判断条件,而非 if——因为线程被唤醒后,缓冲区状态可能已发生变化(如多个消费者被唤醒,缓冲区已空),避免“假唤醒”导致逻辑错误。
3.2 CountDownLatch 实战:主线程等待子线程汇总数据
场景:主线程启动 5 个子线程分别计算数组的不同分段总和,待所有子线程计算完成后,主线程汇总最终结果。
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
public class SumCalculationWithCountDownLatch {
private static int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
private static int totalSum = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 5; // 子线程数量
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
int segmentSize = array.length / threadCount; // 每个线程处理的分段长度
for (int i = 0; i < threadCount; i++) {
int start = i * segmentSize;
int end = (i == threadCount - 1) ? array.length : (i + 1) * segmentSize;
// 启动子线程计算分段和
new Thread(() -> {
int segmentSum = Arrays.stream(array, start, end).sum();
synchronized (SumCalculationWithCountDownLatch.class) {
totalSum += segmentSum; // 累加至总结果(需同步)
}
System.out.println("子线程 " + Thread.currentThread().getName() + " 计算完成,分段和:" + segmentSum);
countDownLatch.countDown(); // 倒计时减 1
}, "T" + i).start();
}
countDownLatch.await(); // 主线程等待所有子线程完成
System.out.println("所有子线程计算完成,数组总和:" + totalSum);
}
}
执行结果:主线程会在 5 个子线程全部调用 countDown() 后才输出最终总和,确保了数据汇总的准确性。
3.3 CyclicBarrier 实战:多线程循环处理数据并汇总
场景:假设有 3 个线程,需完成 2 轮任务:每轮任务中,3 个线程分别处理数据片段,全部处理完成后,由最后一个线程执行“数据合并”的屏障动作,再进入下一轮。
import java.util.concurrent.CyclicBarrier;
public class CycleTaskWithCyclicBarrier {
// 3 个线程参与,屏障动作:合并数据
private static CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("=== 本轮所有线程数据处理完成,执行数据合并 ===");
});
public static void main(String[] args) {
// 启动 3 个线程,每个线程完成 2 轮任务
for (int i = 1; i <= 3; i++) {
int threadId = i;
new Thread(() -> {
for (int round = 1; round <= 2; round++) {
try {
// 模拟数据处理耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + threadId + " 完成第 " + round + " 轮数据处理");
barrier.await(); // 到达集合点,等待其他线程
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
System.out.println("线程 " + threadId + " 完成所有轮次任务");
}).start();
}
}
}
执行结果特征:每轮中,只有当 3 个线程都打印“完成数据处理”后,才会执行“数据合并”的屏障动作;2 轮任务完成后,线程才会结束——体现了 CyclicBarrier 的“循环复用”与“屏障动作”特性。
四、总结:如何选择合适的机制?
三种机制没有绝对的“优劣”,只有“适配场景”的差异,核心选择逻辑如下:
-
简单条件通信选 wait/notify:若需实现线程间的“条件触发”(如缓冲区空/满、数据就绪),且不想引入额外 JUC 类,可使用 wait/notify,但需注意锁的控制与假唤醒问题。
-
单向等待选 CountDownLatch:若场景是“一个线程等待多个线程完成”或“多个线程等待一个线程完成”(如主线程等待子线程初始化),且任务是一次性的,CountDownLatch 是最优选择,API 简洁且高效。
-
循环协同选 CyclicBarrier:若场景是“一组线程互相等待,共同完成多轮任务”,或需要在所有线程到达后执行统一动作(如数据合并、日志记录),CyclicBarrier 的可循环性与屏障动作特性会更贴合需求。
最后需要强调:并发编程的核心是“线程安全”与“高效协同”,无论选择哪种机制,都需明确线程的职责边界、共享资源的访问规则,避免死锁、中断丢失等问题。实际开发中,也可结合线程池(ExecutorService)等工具,进一步提升并发代码的可维护性与性能。

835

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



