Java面试系列文章
面试必知必会(8):CountDownLatch、CyclicBarrier、Semaphore、Exchanger
目录
-
- 一、CountDownLatch:等待多个任务全部完成的"计数器"
-
- [1. 核心定义](#1. 核心定义)
- [2. 核心原理](#2. 核心原理)
- [3. 核心方法](#3. 核心方法)
- [4. 典型使用场景](#4. 典型使用场景)
- [5. 代码实例](#5. 代码实例)
- [6. 注意事项](#6. 注意事项)
- 二、CyclicBarrier:多线程同步的"栅栏"(可重复使用)
-
- [1. 核心定义](#1. 核心定义)
- [2. 核心原理](#2. 核心原理)
- [3. 核心方法](#3. 核心方法)
- [4. 典型使用场景](#4. 典型使用场景)
- [5. 代码实例(模拟多轮同步)](#5. 代码实例(模拟多轮同步))
- 三、Semaphore:控制并发访问数量的"限流工具"
-
- [1. 核心定义](#1. 核心定义)
- [2. 核心原理](#2. 核心原理)
- [3. 核心方法](#3. 核心方法)
- [4. 典型使用场景](#4. 典型使用场景)
- [5. 代码实例(模拟接口限流)](#5. 代码实例(模拟接口限流))
- 四、Exchanger:线程间双向数据交换的"桥梁"
-
- [1. 核心定义](#1. 核心定义)
- [2. 核心原理](#2. 核心原理)
- [3. 核心方法](#3. 核心方法)
- [4. 典型使用场景](#4. 典型使用场景)
- [5. 代码实例(模拟数据交换与校验)](#5. 代码实例(模拟数据交换与校验))
- 五、总结
一、CountDownLatch:等待多个任务全部完成的"计数器"
1. 核心定义
CountDownLatch(倒计时门闩)是一种同步辅助工具,用于让一个或多个线程等待其他多个线程完成各自的任务后,再继续执行自身任务。它的核心是一个递减的计数器,初始化时指定计数器初始值(即需要等待的线程数量)。
注意:CountDownLatch的计数器是一次性的,一旦计数器减至0,后续再调用countDown()方法也不会改变其状态,无法重复使用
2. 核心原理
CountDownLatch基于AQS(AbstractQueuedSynchronizer,抽象队列同步器)实现,其内部维护了一个同步状态state,该state即为计数器的值。
- 初始化时,state = 传入的计数器初始值,此时等待线程(调用await()方法的线程)会被阻塞,加入AQS的等待队列
- 每个任务线程完成后,调用countDown()方法,会将state的值减1(CAS操作,保证线程安全)
- 当state的值减至0时,AQS会唤醒等待队列中所有阻塞的线程,这些线程将竞争锁并继续执行
3. 核心方法
CountDownLatch(int count):构造函数,设置初始计数值void await():阻塞当前线程,直到计数器为 0boolean await(long timeout, TimeUnit unit):带超时的等待void countDown():将计数器的值减1(CAS操作)getCount():获取当前计数器的剩余值(仅用于查看,可能回去后立即被修改)
4. 典型使用场景
CountDownLatch适合"主等从"的场景,即一个主线程等待多个子线程完成任务后,再执行后续逻辑,常见场景如下:
- 场景1:主线程等待所有子线程初始化完成,再启动业务逻辑(如服务启动时,等待多个组件加载完成)
- 场景2:多线程任务执行完成后,主线程统一汇总结果(如多线程统计不同区域的数据,主线程等待所有区域统计完成后,计算总结果)
- 场景3:模拟并发测试(如让100个线程同时执行某个方法,通过CountDownLatch控制所有线程就绪后,再统一开始执行)
5. 代码实例
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* CountDownLatch实例:主线程等待3个子线程完成任务后,汇总结果
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化计数器,指定需要等待的子线程数量(3个)
CountDownLatch countDownLatch = new CountDownLatch(3);
// 2. 启动3个子线程,每个线程执行任务后调用countDown()
for (int i = 1; i <= 3; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("子线程" + taskId + "开始执行任务");
// 模拟任务执行时间(1-3秒,模拟不同任务耗时)
TimeUnit.SECONDS.sleep(taskId);
System.out.println("子线程" + taskId + "任务执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关键:无论任务是否异常,都要调用countDown(),避免主线程永久阻塞
countDownLatch.countDown();
System.out.println("计数器剩余值:" + countDownLatch.getCount());
}
}).start();
}
System.out.println("主线程等待所有子线程完成任务...");
// 3. 主线程阻塞,直到计数器为0(也可使用带超时的await,避免永久阻塞)
// countDownLatch.await(5, TimeUnit.SECONDS); // 超时时间5秒
countDownLatch.await();
// 4. 所有子线程完成后,主线程执行后续逻辑
System.out.println("所有子线程任务全部完成,主线程开始汇总结果");
}
}
输出结果
java
主线程等待所有子线程完成任务...
子线程3开始执行任务
子线程2开始执行任务
子线程1开始执行任务
子线程1任务执行完成
计数器剩余值:2
子线程2任务执行完成
计数器剩余值:1
子线程3任务执行完成
计数器剩余值:0
所有子线程任务全部完成,主线程开始汇总结果
6. 注意事项
- 计数器初始值必须≥0,否则构造方法抛出IllegalArgumentException
- countDown()方法必须在子线程任务完成后调用(建议放在finally块中),否则若子线程抛出异常未执行countDown(),计数器无法减至0,主线程会永久阻塞
await()方法可被中断,若等待线程被中断,会抛出InterruptedException,需妥善处理(如停止后续逻辑、记录日志)CountDownLatch是一次性的,计数器归0后,再调用countDown()无效,若需重复使用,需重新创建实例(后续CyclicBarrier可解决此问题)
二、CyclicBarrier:多线程同步的"栅栏"(可重复使用)
1. 核心定义
CyclicBarrier(循环栅栏)也是一种同步辅助工具,用于让多个线程在某个"栅栏点"处相互等待,直到所有线程都到达该栅栏点后,所有线程才会同时继续执行。
此外,CyclicBarrier 支持在所有线程到达栅栏点后执行一个回调任务(Runnable)。
与CountDownLatch的核心区别:
- CountDownLatch是"主等从",主线程等待子线程完成;CyclicBarrier是"平等等待",所有线程相互等待,无主从之分
- CountDownLatch计数器一次性,归0后无法复用;CyclicBarrier栅栏可重复使用(计数器归0后,会自动重置为初始值,支持多轮同步)
- CountDownLatch的countDown()可在任意线程中调用;CyclicBarrier的"到达栅栏"操作,只能由参与同步的线程自身调用await()触发
2. 核心原理
CyclicBarrier同样基于AQS实现,其内部维护了两个核心变量:
- parties:参与同步的线程数量(初始化时指定)
- count:当前等待到达栅栏的线程数量(初始值=parties,每有一个线程调用await(),count减1)
工作流程:
- 初始化CyclicBarrier时,指定parties(参与线程数)和一个可选的回调函数Runnable(栅栏触发后,会由最后一个到达栅栏的线程执行该任务)
- 每个参与线程执行完自身任务后,调用await()方法,此时线程会阻塞(进入AQS等待队列),同时count减1
- 当count减至0时,所有阻塞的线程被唤醒,如何存在回调函数Runnable任务(仅最后一个到达的线程执行)
- 所有线程唤醒后,CyclicBarrier的count会自动重置为parties,支持下一轮同步(可重复使用)
3. 核心方法
CyclicBarrier(int parties):指定参与线程数CyclicBarrier(int parties, Runnable barrierAction):指定线程数和回调函数int await():等待所有线程到达栅栏点int await(long timeout, TimeUnit unit):带超时的等待
4. 典型使用场景
CyclicBarrier适合"多线程协同完成多轮任务"的场景,所有线程必须同步到达某个节点后,才能进入下一轮,常见场景如下:
- 场景1:多线程分阶段执行任务(如模拟比赛,所有选手到达起点(栅栏1)后,同时开始比赛;所有选手到达终点(栅栏2)后,同时公布成绩)
- 场景2:多线程数据并行处理(如处理一个大文件,分成多个分片,每个线程处理一个分片,所有线程处理完成(栅栏点)后,再由一个线程汇总分片结果,然后进入下一轮处理)
- 场景3:测试多线程同步性能(如让多个线程重复执行"执行任务→等待栅栏"的流程,模拟多轮同步场景)
5. 代码实例(模拟多轮同步)
java
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("所有线程已就位,开始下一阶段任务!");
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 到达第一阶段");
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " 执行第二阶段");
Thread.sleep(1000);
barrier.await(); // 再次等待,体现"可重用"
System.out.println(Thread.currentThread().getName() + " 完成全部任务");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
三、Semaphore:控制并发访问数量的"限流工具"
1. 核心定义
Semaphore(信号量)是一种用于控制同时访问某个共享资源的线程数量的同步工具,本质上是一个"许可证"计数器。它通过发放许可证的方式,限制并发访问的线程数:线程需要先获取许可证才能访问资源,访问完成后释放许可证,其他线程才能获取许可证继续访问。
核心作用:限流、控制并发度,避免因线程过多导致的资源耗尽(如数据库连接池、接口限流、文件读写并发控制)。
2. 核心原理
Semaphore基于AQS实现,其内部维护的state即为"可用许可证数量",初始化时指定许可证总数。
- 初始化时,state = 许可证总数(permits)
- 线程调用acquire()方法获取许可证:若state>0,state减1(获取成功,线程继续执行);若state=0,线程阻塞,加入AQS等待队列
- 线程访问资源完成后,调用release()方法释放许可证:state加1(释放成功),同时唤醒AQS等待队列中阻塞的线程,使其有机会获取许可证
关键细节:Semaphore支持"公平锁"和"非公平锁"模式(构造方法指定):
- 公平模式(fair=true):等待时间最长的线程优先获取许可证(FIFO),避免线程饥饿,但性能略差
- 非公平模式(fair=false,默认):线程获取许可证时不按等待顺序,直接竞争,性能更好,但可能导致某些线程长期无法获取许可证(饥饿)
3. 核心方法
Semaphore(int permits):创建具有指定许可数的信号量Semaphore(int permits, boolean fair):指定是否公平void acquire():获取一个许可(阻塞)void acquire(int permits):获取多个许可void release():释放一个许可boolean tryAcquire():尝试获取许可,不阻塞int availablePermits():返回当前可用许可数
4. 典型使用场景
Semaphore的核心场景是"限流",控制并发访问的线程数,常见场景如下:
- 场景1:数据库连接池限流(如数据库最多支持10个并发连接,用Semaphore初始化10个许可证,每个线程获取许可证后获取连接,用完释放许可证,避免连接耗尽)
- 场景2:接口限流(如某个接口每秒最多允许5个请求并发访问,用Semaphore控制,超过5个请求则等待或拒绝)
- 场景3:共享资源并发控制(如多个线程读写同一个文件,用Semaphore限制同时读写的线程数,避免文件损坏)
5. 代码实例(模拟接口限流)
java
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Semaphore实例:模拟接口限流,最多允许3个线程同时访问接口
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 1. 初始化信号量:3个许可证(最多3个线程同时访问),公平模式(避免线程饥饿)
Semaphore semaphore = new Semaphore(3, true);
// 2. 启动10个线程,模拟10个并发请求访问接口
for (int i = 1; i <= 10; i++) {
int requestId = i;
new Thread(() -> {
try {
System.out.println("请求" + requestId + ":尝试访问接口,获取许可证");
// 3. 尝试获取许可证(带超时,避免永久阻塞,超时时间2秒)
boolean acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS);
if (!acquired) {
System.out.println("请求" + requestId + ":获取许可证超时,接口访问失败(限流)");
return;
}
// 4. 获取许可证成功,访问接口(模拟接口耗时)
System.out.println("请求" + requestId + ":获取许可证成功,开始访问接口");
TimeUnit.SECONDS.sleep(1); // 模拟接口处理耗时1秒
System.out.println("请求" + requestId + ":接口访问完成");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("请求" + requestId + ":获取许可证被中断");
} finally {
// 5. 释放许可证(无论访问成功与否,都要释放,避免许可证泄漏)
// 注意:只有获取到许可证的线程,才需要释放(此处用acquired判断)
if (semaphore.availablePermits() < 3) { // 简单判断是否获取到许可证
semaphore.release();
System.out.println("请求" + requestId + ":释放许可证,当前可用许可证:" + semaphore.availablePermits());
}
}
}).start();
// 模拟请求间隔(0.5秒一个请求,模拟高并发)
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
四、Exchanger:线程间双向数据交换的"桥梁"
1. 核心定义
Exchanger(交换器)是一种用于两个线程之间双向交换数据的同步工具,它提供了一个交换点,两个线程可以在该交换点交换各自的数据:线程A到达交换点后,会阻塞等待线程B到达;线程B到达交换点后,会将自己的数据传递给线程A,同时获取线程A的数据,然后两个线程同时继续执行。
核心特点:
- 仅支持
两个线程之间的数据交换,多线程使用时会出现不可预期的结果(如线程A与线程B交换,线程C等待时可能被线程D唤醒,交换错误) - 交换的数据是双向的:线程A传递数据给线程B,同时线程B传递数据给线程A,不同于简单的线程间通信(如wait/notify单向传递)
2. 核心原理
Exchanger内部维护了一个"交换槽"(slot),用于存储等待交换的数据,以及一个等待线程的引用,其工作流程如下:
- 线程A调用exchange(V x)方法,将自己的数据x放入交换槽,然后阻塞等待线程B
- 线程B调用exchange(V y)方法,发现交换槽中已有数据(线程A的数据x),则将自己的数据y放入交换槽,同时获取x,然后唤醒线程A
- 线程A被唤醒后,获取交换槽中的y(线程B的数据),两个线程同时继续执行,完成数据交换
补充:若只有一个线程调用exchange()方法,该线程会一直阻塞,直到有另一个线程调用exchange(),或被中断、超时
3. 核心方法
Exchanger():构造方法,初始化交换器V exchange(V x):交换数据,阻塞直到配对成功V exchange(V x, long timeout, TimeUnit unit):带超时的交换
4. 典型使用场景
Exchanger适合"两个线程需要相互交换数据"的场景,常见场景如下:
- 场景1:数据校验(如线程A生成数据,线程B校验数据,线程A将数据传递给线程B,线程B将校验结果传递给线程A)
- 场景2:数据交换(如线程A处理一半的数据,线程B处理另一半的数据,两者交换处理结果,合并为最终结果)
- 场景3:生产者-消费者变体(如生产者生产数据,消费者消费数据后返回处理反馈,两者通过Exchanger交换"数据"和"反馈")
5. 代码实例(模拟数据交换与校验)
java
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
/**
* Exchanger实例:两个线程交换数据(线程A生成数据,线程B校验数据,交换数据与校验结果)
*/
public class ExchangerDemo {
public static void main(String[] args) {
// 1. 初始化交换器,泛型指定交换的数据类型(此处为String)
Exchanger<String> exchanger = new Exchanger<>();
// 线程1:生成数据,等待与线程2交换(交换数据与校验结果)
new Thread(() -> {
String data = null;
try {
// 模拟生成数据耗时
TimeUnit.SECONDS.sleep(1);
data = "用户信息:id=1001,name=张三";
System.out.println("线程1:生成数据,准备交换:" + data);
// 2. 调用exchange(),放入数据,阻塞等待线程2交换
// 可选:带超时的exchange,避免永久阻塞
// String result = exchanger.exchange(data, 3, TimeUnit.SECONDS);
String result = exchanger.exchange(data);
// 3. 交换完成,获取线程2的校验结果
System.out.println("线程1:交换完成,获取线程2的校验结果:" + result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("线程1:交换数据失败");
}
}, "线程1").start();
// 线程2:接收线程1的数据,进行校验,然后交换校验结果
new Thread(() -> {
String checkResult = null;
try {
// 模拟准备校验耗时
TimeUnit.SECONDS.sleep(2);
System.out.println("线程2:准备接收线程1的数据,进行校验");
// 2. 调用exchange(),阻塞等待线程1的数据,同时传入校验结果
String data = exchanger.exchange("校验结果:数据格式正确,无异常");
// 3. 交换完成,获取线程1的数据,模拟校验(此处简化校验逻辑)
System.out.println("线程2:交换完成,获取线程1的数据:" + data);
// 模拟校验逻辑
if (data.contains("id=") && data.contains("name=")) {
checkResult = "校验结果:数据格式正确,无异常";
} else {
checkResult = "校验结果:数据格式错误,请重新生成";
}
System.out.println("线程2:校验完成,校验结果:" + checkResult);
} catch (Exception e) {
e.printStackTrace();
System.out.println("线程2:交换数据失败");
}
}, "线程2").start();
}
}
五、总结
本文详细解析了Java并发编程中四个常用工具类的核心细节,它们各自聚焦不同的并发场景,封装了复杂的同步逻辑,帮助开发者简化并发代码编写:
CountDownLatch:适合"主等从",一次性等待多任务完成,无需复用CyclicBarrier:适合"多线程平等同步",支持多轮复用,适合分阶段任务Semaphore:适合"限流控制",控制并发访问数量,避免资源耗尽Exchanger:适合"两个线程双向数据交换",原子性交换,简化线程间双向通信