在 Java 并发编程体系中,java.util.concurrent(JUC)包提供了一系列开箱即用的并发工具类。它们基于 AQS(AbstractQueuedSynchronizer)等底层同步框架构建,封装了复杂的多线程协同逻辑,让开发者可以快速实现线程等待、限流、数据交换等常见并发场景。
其中 CountDownLatch、CyclicBarrier、Semaphore、Exchanger 是面试与工程实践中最高频的四大工具类,分别对应「等待完成」「同步屏障」「流量控制」「线程交换」四类核心协同场景。本文将从功能定义、源码实现、使用场景、代码示例四个维度逐一拆解,并在最后汇总高频面试问题与标准答案。
一、CountDownLatch:一次性倒计时闭锁
1. 核心作用
CountDownLatch 是一个一次性使用的倒计时同步器,它允许一个或多个线程阻塞等待,直到其他线程完成一组操作后再继续执行。
可以把它理解为一个倒计时闸门:初始设定一个计数,每个任务完成就扣减 1;当计数归零时,闸门打开,所有等待的线程同时被唤醒继续执行。
2. 源码实现原理
CountDownLatch 的核心是内部类 Sync,它继承自 AQS,基于 AQS 的共享模式实现同步逻辑。
- 构造函数传入的计数 count,会赋值给 AQS 的
state变量,代表当前剩余的计数; countDown()方法:本质是执行一次「共享锁释放」,将 state 原子性减 1;当 state 减到 0 时,唤醒 AQS 队列中所有阻塞的等待线程;await()方法:本质是尝试获取共享锁,只要 state 不为 0,当前线程就会进入 AQS 同步队列阻塞等待。
核心源码片段解析:
java
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
// 尝试获取共享锁:state==0 表示获取成功,否则失败
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 尝试释放共享锁:自旋CAS将state减1,减到0时返回true,触发唤醒
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
两个关键设计细节:
tryAcquireShared不做减法,只判断 state 是否为 0,完全符合「等待计数归零」的语义;tryReleaseShared通过自旋 CAS 保证多线程下计数扣减的原子性,只有当计数从 1 变为 0 时才返回 true,触发 AQS 唤醒所有等待线程。
3. 典型使用场景
- 并行任务汇总:主线程开启多个子线程执行任务,等待所有子线程全部完成后再汇总结果;
- 资源初始化:服务启动时,等待多个资源加载模块全部加载完成后,再对外提供服务;
- 并发压测:让所有工作线程准备完成后同时启动,模拟真正的并发请求。
4. 代码示例
java
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始计数:3个任务
CountDownLatch latch = new CountDownLatch(3);
// 启动3个工作线程
for (int i = 1; i <= 3; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("任务" + taskId + "开始执行");
Thread.sleep(1000);
System.out.println("任务" + taskId + "执行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 计数减1
latch.countDown();
}
}).start();
}
System.out.println("主线程等待所有任务完成...");
// 主线程阻塞,直到计数归零
latch.await();
System.out.println("所有任务执行完成,主线程继续执行");
}
}
二、CyclicBarrier:可循环使用的同步屏障
1. 核心作用
CyclicBarrier(循环栅栏)是一个可重复使用的同步屏障,它让一组线程互相等待,直到所有线程都到达屏障点(调用 await 方法)后,所有线程才会同时突破屏障继续执行。
和 CountDownLatch 最大的不同是:它支持「循环使用」,一轮等待完成后可以重置计数,进入下一轮;同时构造函数支持传入一个 Runnable 任务,当所有线程到达屏障时,由最后一个到达的线程优先执行该任务。
2. 源码实现原理
CyclicBarrier 没有直接基于 AQS 实现,而是基于 ReentrantLock + Condition 实现等待唤醒逻辑,核心方法是内部的 dowait。
核心成员变量:
count:当前剩余需要等待的线程数,初始值为 parties(参与线程总数);parties:参与的总线程数,用于循环重置;barrierAction:屏障触发时执行的任务,可为 null;generation:代,用于实现屏障的循环复用,每一轮对应一个 generation,屏障打破或重置时会换代。
核心执行逻辑:
- 线程调用 await 后,先加锁,将 count 减 1;
- 如果 count 减为 0,说明是最后一个到达的线程:若设置了 barrierAction 则先执行该任务,随后调用
nextGeneration()唤醒所有等待线程,重置 count,创建新的 generation,完成一轮循环; - 如果 count 不为 0,调用
condition.await()进入条件队列阻塞等待; - 若等待过程中线程被中断、超时,或者屏障被其他线程打破,会抛出异常并标记屏障为损坏状态。
3. 典型使用场景
- 多阶段并行计算:分阶段的数据计算,每一轮所有线程计算完成后,再统一进入下一轮;
- 多人游戏匹配:所有玩家都加载完成后,才统一进入游戏对局;
- 并发测试:多线程执行任务,每轮结束后统一统计结果,再开启下一轮。
4. 代码示例
java
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 3个线程到达屏障后,先执行汇总任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障,执行本轮汇总操作");
});
for (int i = 1; i <= 3; i++) {
int taskId = i;
new Thread(() -> {
try {
// 第一轮
System.out.println("线程" + taskId + "执行第一轮任务");
Thread.sleep(taskId * 500);
barrier.await(); // 等待所有线程到达
System.out.println("线程" + taskId + "突破第一轮屏障");
// 第二轮(循环复用)
System.out.println("线程" + taskId + "执行第二轮任务");
Thread.sleep(taskId * 300);
barrier.await();
System.out.println("线程" + taskId + "突破第二轮屏障");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
三、Semaphore:信号量与流量控制
1. 核心作用
Semaphore(信号量)用来控制同时访问特定资源的线程数量,本质是一个「许可计数器」,通过控制许可的发放与回收,实现对并发访问的流量控制。
它支持公平与非公平两种模式,默认是非公平模式;和独占锁不同,它允许多个线程同时访问资源,是共享锁的典型实现。
2. 源码实现原理
Semaphore 同样基于 AQS 的共享模式实现,内部有三个核心内部类:抽象 Sync、NonfairSync(非公平)、FairSync(公平)。
- AQS 的
state变量代表剩余可用许可数; acquire()方法:尝试获取许可,state 减去对应数量,如果剩余许可不足,线程进入 AQS 队列阻塞;release()方法:释放许可,state 加上对应数量,同时唤醒队列中等待的线程。
公平与非公平的核心差异:
- 非公平模式(默认):线程尝试获取许可时,直接 CAS 扣减许可,不管队列中有没有等待的线程,吞吐更高;
- 公平模式:获取许可前先调用
hasQueuedPredecessors()判断同步队列中是否有等待更久的线程,如果有就排队,保证先到先得。
核心源码片段(非公平获取):
java
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 许可不足直接返回负数;许可充足则CAS更新并返回剩余值
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
面试冷知识:
release方法不会校验释放的许可是否是当前线程获取的,理论上可以通过多次 release 动态增加总许可数。
3. 典型使用场景
- 接口限流:控制高并发场景下同时访问后端资源的请求数,保护系统稳定性;
- 连接池控制:比如数据库连接池、Redis 连接池,控制同时建立的连接数量;
- 资源池管控:对有限的公共资源(如文件句柄、硬件设备)做并发访问控制。
4. 代码示例
java
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
// 3个许可,模拟最多3个线程同时访问
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
int userId = i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("用户" + userId + "获取访问权限,开始执行");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("用户" + userId + "执行结束,释放权限");
semaphore.release(); // 释放许可
}
}).start();
}
}
}
四、Exchanger:线程间数据交换器
1. 核心作用
Exchanger 是一个用于两个线程之间交换数据 的同步工具类:两个线程通过 exchange() 方法交换数据,第一个到达的线程会阻塞等待,直到第二个线程也到达交换点,双方完成数据交换后,再各自继续执行。
它是「两两配对」的同步工具,设计上只支持两个线程参与交换。
2. 源码实现原理
Exchanger 的实现相对独立,核心基于 CAS + 自旋 + 槽位(Slot)机制;JDK 为了优化高并发性能,还引入了「竞技场(Arena)」多槽位结构,减少多线程竞争。
核心执行逻辑:
- 第一个线程调用
exchange(V x)时,会将自己的数据存入槽位,然后自旋 / 阻塞等待第二个线程; - 第二个线程调用
exchange(V x)时,发现槽位中已有数据,就取出对方的数据,同时把自己的数据放入槽位,然后唤醒第一个等待的线程; - 两个线程分别拿到对方的数据,完成交换。
单槽位在低并发下性能好,高并发下 CAS 竞争激烈;Arena 模式会使用多个槽位,线程通过哈希分配到不同槽位交换,有效降低冲突。
3. 典型使用场景
- 遗传算法:两个线程分别演化不同的种群,到同步点交换最优个体;
- 数据校对:两个线程分别处理同一份数据的两份副本,交换结果进行校验;
- 管道式交互:两个线程在不同阶段交换中间结果,协同完成任务。
4. 代码示例
java
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
// 线程A
new Thread(() -> {
try {
String dataA = "线程A的业务数据";
System.out.println("线程A准备交换数据:" + dataA);
String result = exchanger.exchange(dataA); // 阻塞等待对方
System.out.println("线程A交换后得到:" + result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// 线程B
new Thread(() -> {
try {
Thread.sleep(1000); // 模拟业务处理耗时
String dataB = "线程B的业务数据";
System.out.println("线程B准备交换数据:" + dataB);
String result = exchanger.exchange(dataB);
System.out.println("线程B交换后得到:" + result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
五、高频面试问题汇总
1. CountDownLatch 和 CyclicBarrier 有什么区别?
这是最高频的面试题,核心从 5 个维度区分:
- 复用性:CountDownLatch 是一次性的,计数归零后无法重置;CyclicBarrier 可以循环使用,每轮结束后自动重置计数。
- 等待对象:CountDownLatch 是「一个 / 多个线程等待其他线程完成」,通常是主线程等工作线程;CyclicBarrier 是「一组线程互相等待」,所有线程地位平等,一起到达屏障后一起继续。
- 底层实现:CountDownLatch 直接基于 AQS 共享模式实现;CyclicBarrier 基于 ReentrantLock + Condition 实现。
- 附加功能:CyclicBarrier 支持传入屏障任务(Runnable),所有线程到达后执行;CountDownLatch 没有该功能。
- 异常机制:CyclicBarrier 有屏障损坏(BrokenBarrierException)机制,线程中断、超时会标记屏障失效;CountDownLatch 无此概念。
2. Semaphore 的公平模式和非公平模式有什么区别?
- 非公平模式(默认):线程获取许可时直接尝试 CAS 扣减,不考虑同步队列中是否有等待线程,可能出现线程插队,优点是吞吐量大、性能高。
- 公平模式:每次获取许可前都会检查 AQS 队列中是否有等待更久的前驱线程,如果有则主动排队,保证先到先得,缺点是吞吐量更低,调度开销更大。
3. Semaphore 的 release () 可以动态增加许可吗?
可以。Semaphore 的 release() 方法只会原子性地增加 state 的值,不会校验释放的许可是否由当前线程持有,也不会限制总许可数不超过初始值。
这也是它和锁的核心区别之一:锁的释放必须由持有锁的线程执行,而 Semaphore 的许可释放没有线程绑定关系。
4. CyclicBarrier 是如何实现循环复用的?
CyclicBarrier 通过「代(Generation)」的概念实现循环:
- 每一轮等待对应一个 generation 对象;
- 当最后一个线程到达屏障、所有线程被唤醒后,会创建新的 generation 对象,并将 count 重置为初始的 parties 值;
- 后续线程调用 await 会进入新一代的等待,从而实现循环使用。
如果某一轮中有线程中断、超时,会标记当前 generation 为 broken,所有等待线程抛出 BrokenBarrierException,屏障失效。
5. 四个工具类中,哪些直接基于 AQS 实现?
- CountDownLatch:直接基于 AQS 共享模式实现
- Semaphore:直接基于 AQS 共享模式实现
- CyclicBarrier:基于 ReentrantLock + Condition,间接依赖 AQS
- Exchanger:基于 CAS + 槽位机制,不依赖 AQS
6. Exchanger 支持多个线程交换数据吗?
不支持。Exchanger 设计上就是用于两个线程之间的数据交换,多个线程调用时会两两随机配对交换,无法实现多线程的数据汇聚。如果需要多线程汇聚数据,应该使用 CyclicBarrier 或 CountDownLatch。
六、总结
四大并发工具类是 JUC 体系中的高频实用组件,本质都是对多线程协同逻辑的封装:
- CountDownLatch 适合「一等多」的一次性等待场景;
- CyclicBarrier 适合「多等多」的循环同步场景;
- Semaphore 适合资源限流、并发数控制场景;
- Exchanger 适合双线程的数据交换场景。