
在 Java 并发编程中,synchronized虽然能解决线程同步问题,但功能单一、灵活性差,面对复杂场景(如多线程等待、流量控制、数据交换)时力不从心。JDK 提供的CountDownLatch、CyclicBarrier、Semaphore、Exchanger四大并发工具类,基于 AQS(队列式同步器)实现,能更优雅地处理各类并发场景。本文用 “生活场景 + 代码示例” 的方式,带你吃透这 4 个工具类,面试和工作都能用得上!
一、为什么需要并发工具类?—— 解决synchronized的痛点
synchronized作为 Java 基础的同步锁,存在 3 个明显局限:
- 只能实现 “互斥”(同一时间一个线程访问资源),无法实现 “协作”(多个线程按顺序执行);
- 没有超时机制,线程可能永久阻塞;
- 功能单一,无法满足流量控制、数据交换等复杂需求。
而并发工具类的核心价值的是 **“线程协作与精细化控制”**,让多线程不再是 “各自为战”,而是能按预期流程协同工作,同时提供超时、回调等高级特性,让并发编程更安全、更灵活。
二、4 大核心并发工具类:场景 + 代码 + 原理
1. CountDownLatch:倒计时锁 ——“等待其他线程完成,再行动”
核心定位
允许 1 个或多个线程等待,直到其他N个线程完成任务后,再继续执行。本质是 “倒计数器”,计数器归 0 时,等待线程被唤醒。
生活场景
- 考试结束:老师(等待线程)要等所有学生(工作线程)交卷后,才开始批改试卷;
- 田径赛跑:10 名运动员(工作线程)等待裁判(控制线程)发令后,同时起跑。
核心方法
- CountDownLatch(int count):构造方法,初始化计数器(需等待的线程数);
- countDown():工作线程调用,计数器减 1(任务完成);
- await():等待线程调用,阻塞直到计数器为 0;
- await(long timeout, TimeUnit unit):带超时的等待,避免永久阻塞。
实战代码
场景 1:主线程等待 10 个工作线程完成后汇总结果
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo1 {
public static void main(String[] args) throws InterruptedException {
int threadCount = 10; // 10个工作线程
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 模拟工作任务(如数据查询、文件处理)
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":任务执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 计数器减1(必须放在finally,确保任务完成后执行)
}
}, "工作线程" + i).start();
}
System.out.println("主线程:等待所有工作线程完成…");
latch.await(); // 阻塞等待计数器为0
System.out.println("主线程:所有任务执行完毕,开始汇总结果");
}
}
运行结果:
主线程:等待所有工作线程完成…
工作线程0:任务执行完毕
工作线程1:任务执行完毕
…(中间省略8个线程)
工作线程9:任务执行完毕
主线程:所有任务执行完毕,开始汇总结果
场景 2:10 个线程等待指令,同时执行
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1); // 计数器初始为1(等待1个信号)
int threadCount = 10;
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":准备就绪,等待指令");
latch.await(); // 阻塞等待计数器为0
System.out.println(Thread.currentThread().getName() + ":收到指令,开始执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "运动员" + i).start();
}
// 模拟裁判准备时间
Thread.sleep(2000);
System.out.println("主线程(裁判):发令!");
latch.countDown(); // 计数器减1(变为0,唤醒所有等待线程)
}
}
运行结果:
运动员0:准备就绪,等待指令
运动员1:准备就绪,等待指令
…(中间省略8个线程)
运动员9:准备就绪,等待指令
主线程(裁判):发令!
运动员0:收到指令,开始执行
运动员1:收到指令,开始执行
…(所有线程同时执行)
关键注意
- 计数器只能使用一次,归 0 后再调用countDown()无效;
- countDown()必须放在finally块中,确保工作线程无论是否异常,都会 decrement 计数器,避免等待线程永久阻塞。
2. CyclicBarrier:循环屏障 ——“大家到齐了,再一起出发”
核心定位
让N个线程到达 “屏障” 后阻塞,直到所有线程都到达屏障,屏障才会解除,所有线程同时继续执行。与CountDownLatch的核心区别是:计数器可循环使用。
生活场景
- 团队旅游:导游(屏障)要等所有游客(线程)到齐后,才出发去下一个景点;
- 多阶段任务:多个线程先完成第一阶段任务,到屏障处集合,再同时开始第二阶段任务。
核心方法
- CyclicBarrier(int parties):构造方法,指定参与的线程数(屏障触发条件);
- CyclicBarrier(int parties, Runnable barrierAction):带回调的构造方法,所有线程到达屏障后,先执行barrierAction(如日志记录、资源初始化);
- await():线程到达屏障后调用,阻塞直到所有线程都到达;
- await(long timeout, TimeUnit unit):带超时的等待。
实战代码
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int threadCount = 5; // 5个线程参与
// 屏障触发时,执行回调(所有线程到齐后执行)
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("\\n===== 所有线程已到齐,屏障解除!=====\\n");
});
for (int i = 0; i < threadCount; i++) {
int finalI = i;
new Thread(() -> {
try {
// 模拟线程到达屏障前的任务(如数据预处理)
Thread.sleep((long) (Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + ":已到达屏障(完成第" + finalI + "阶段任务)");
barrier.await(); // 阻塞等待其他线程
// 屏障解除后,执行后续任务
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":继续执行第" + (finalI + 1) + "阶段任务");
} catch (Exception e) {
e.printStackTrace();
}
}, "工作线程" + i).start();
}
}
}
运行结果:
工作线程2:已到达屏障(完成第2阶段任务)
工作线程0:已到达屏障(完成第0阶段任务)
工作线程1:已到达屏障(完成第1阶段任务)
工作线程3:已到达屏障(完成第3阶段任务)
工作线程4:已到达屏障(完成第4阶段任务)
===== 所有线程已到齐,屏障解除!=====
工作线程4:继续执行第5阶段任务
工作线程0:继续执行第1阶段任务
工作线程2:继续执行第3阶段任务
工作线程1:继续执行第2阶段任务
工作线程3:继续执行第4阶段任务
关键注意
- 计数器可循环使用:屏障解除后,计数器自动重置,可再次用于下一轮线程协作;
- 若某个线程中断或超时,屏障会被破坏,其他线程会抛出BrokenBarrierException,需捕获处理。
3. Semaphore:信号量 ——“流量控制,最多 N 个线程同时访问”
核心定位
控制同一时间访问某个资源的线程数,本质是 “许可计数器”,线程需先获取许可才能访问资源,用完后释放许可。
生活场景
- 停车场:最多容纳 100 辆车(许可数 100),车辆(线程)进入前需获取车位(许可),离开时释放车位;
- 接口限流:同一时间最多允许 10 个请求(线程)访问支付接口,避免服务过载。
核心方法
- Semaphore(int permits):构造方法,指定最大许可数(同时访问的线程数);
- acquire():获取 1 个许可,若无许可则阻塞;
- tryAcquire(long timeout, TimeUnit unit):尝试在指定时间内获取许可,成功返回true,失败返回false;
- release():释放 1 个许可,唤醒等待的线程;
- availablePermits():获取当前可用的许可数。
实战代码
场景:接口限流(同一时间最多 3 个线程访问)
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
int maxPermits = 3; // 最多3个线程同时访问
Semaphore semaphore = new Semaphore(maxPermits);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 模拟10个请求线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 尝试3秒内获取许可,避免永久阻塞
try {
if (semaphore.tryAcquire(3, java.util.concurrent.TimeUnit.SECONDS)) {
// 模拟接口处理时间(1-2秒)
Thread.sleep((long) (Math.random() * 1000 + 1000));
System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + ":获取许可,接口访问成功");
} else {
System.out.println(sdf.format(new Date()) + " | " + Thread.currentThread().getName() + ":获取许可超时,接口访问失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可(必须放在finally,避免许可泄露)
if (semaphore.hasQueuedThreads()) {
semaphore.release();
}
}
}, "请求线程" + i).start();
}
}
}
运行结果:
2024-05-20 15:30:01 | 请求线程0:获取许可,接口访问成功
2024-05-20 15:30:01 | 请求线程1:获取许可,接口访问成功
2024-05-20 15:30:01 | 请求线程2:获取许可,接口访问成功
2024-05-20 15:30:02 | 请求线程3:获取许可,接口访问成功
2024-05-20 15:30:02 | 请求线程4:获取许可,接口访问成功
…(后续线程依次获取释放的许可)
关键注意
- 许可必须手动释放,且要放在finally块中,避免因线程异常导致许可泄露(可用许可越来越少);
- 当许可数为 1 时,Semaphore等价于互斥锁(synchronized),但更灵活(支持超时、非阻塞获取)。
4. Exchanger:数据交换器 ——“两个线程的双向数据交换”
核心定位
用于两个线程之间的双向数据交换,线程到达交换点后,会阻塞等待另一个线程,直到两个线程都到达,才交换彼此的数据。
生活场景
- 快递交换:两个快递员(线程)在指定地点碰面,交换各自的包裹(数据);
- 数据校验:线程 A 读取文件 A 的数据,线程 B 读取文件 B 的数据,交换后互相校验。
核心方法
- Exchanger<V>:泛型类,指定交换的数据类型;
- exchange(V x):线程调用,将数据x传递给对方,同时接收对方的数据,阻塞直到对方到达交换点;
- exchange(V x, long timeout, TimeUnit unit):带超时的交换,超时抛出异常。
实战代码
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>(); // 交换String类型数据
// 线程1:发送数据"A",接收线程2的数据
new Thread(() -> {
try {
String data = "A";
System.out.println(Thread.currentThread().getName() + ":准备交换的数据:" + data);
// 阻塞等待线程2,交换数据
String receivedData = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + ":交换后收到的数据:" + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "交换线程1").start();
// 线程2:发送数据"B",接收线程1的数据
new Thread(() -> {
try {
String data = "B";
System.out.println(Thread.currentThread().getName() + ":准备交换的数据:" + data);
// 模拟延迟到达交换点
Thread.sleep(1000);
String receivedData = exchanger.exchange(data);
System.out.println(Thread.currentThread().getName() + ":交换后收到的数据:" + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "交换线程2").start();
}
}
运行结果:
交换线程1:准备交换的数据:A
交换线程2:准备交换的数据:B
交换线程1:交换后收到的数据:B
交换线程2:交换后收到的数据:A
关键注意
- 仅支持两个线程交换,多个线程调用exchange()会导致数据交换混乱;
- 若只有一个线程到达交换点,会一直阻塞(或超时),需确保两个线程都能正常到达。
三、高频面试:4 大工具类核心区别
| CountDownLatch | 1 个 / 多个线程等待 N 个线程完成 | 一次性使用 | 任务汇总、指令下发 | 等待线程不参与任务,仅等待;计数器不可循环 |
| CyclicBarrier | N 个线程互相等待,到齐后同时执行 | 可循环使用 | 多阶段任务、团队协作 | 所有线程都参与任务,到达屏障后继续;计数器可重置 |
| Semaphore | 控制同时访问资源的线程数 | 可重复获取释放 | 接口限流、资源池控制 | 不涉及线程等待,仅流量控制 |
| Exchanger | 两个线程双向数据交换 | 无计数器 | 数据交换、双向校验 | 仅支持两个线程,专注数据交换 |
四、实战避坑指南
五、总结
Java 并发工具类是synchronized的进阶替代方案,专注于 “线程协作” 和 “精细化控制”:
- 需 “等待其他线程完成” 用CountDownLatch;
- 需 “线程互相等待、循环协作” 用CyclicBarrier;
- 需 “流量控制、限制并发数” 用Semaphore;
- 需 “两个线程数据交换” 用Exchanger。
这些工具类底层都基于 AQS 实现,无需关注复杂的锁机制,只需根据场景选择合适的工具,就能让并发编程更简洁、更安全。建议结合实际场景多敲代码练习,理解它们的适用边界,面试时就能从容应对,工作中也能快速解决并发问题。
网硕互联帮助中心






评论前必须登录!
注册