Java并发工具类详解:Semaphore、CyclicBarrier与CountDownLatch
Java并发工具类详解:Semaphore、CyclicBarrier与CountDownLatch
在多线程编程中,线程间的同步与协作是核心难题。Java并发包(java.util.concurrent
)提供了多种工具类,简化了复杂场景下的线程协调逻辑。本文将深入解析三个常用工具类——Semaphore
、CyclicBarrier
和CountDownLatch
,通过场景案例、代码实现和对比分析,理清它们的适用场景与核心区别。
一、Semaphore:控制并发访问的"信号灯"
1. 核心功能
Semaphore
(信号量)用于限制同时访问某个资源的线程数量,类似"限流"机制。它维护一组"许可"(permits),线程需先获取许可才能访问资源,访问完成后释放许可供其他线程使用。
2. 适用场景
- 控制并发请求数(如接口限流)
- 管理资源池访问(如数据库连接池、线程池)
- 实现生产者-消费者模型中的缓冲区边界控制
3. 代码案例:数据库连接池限流
假设数据库最多支持3个并发连接,超过则需等待:
import java.util.concurrent.Semaphore;public class SemaphoreDemo {public static void main(String[] args) {// 初始化3个许可(最多3个线程同时访问)Semaphore semaphore = new Semaphore(3);System.out.println("=== 数据库连接池初始化完成,最大并发连接数:3 ===");// 模拟10个线程竞争访问数据库for (int i = 0; i < 10; i++) {final int threadId = i + 1;new Thread(() -> {try {System.out.println("线程" + threadId + "尝试获取数据库连接...");// 获取许可(若满则阻塞等待)semaphore.acquire();System.out.println("线程" + threadId + "✅ 获取到数据库连接,开始执行SQL操作...");Thread.sleep(1000); // 模拟数据库操作耗时System.out.println("线程" + threadId + "📝 SQL操作执行完成");} catch (InterruptedException e) {e.printStackTrace();} finally {// 释放许可(必须在finally中执行,避免资源泄漏)semaphore.release();System.out.println("线程" + threadId + "🔄 释放数据库连接,当前可用连接数:" + semaphore.availablePermits());}}).start();// 错开线程启动时间,便于观察输出Thread.sleep(200);}}
}
输出示例:
=== 数据库连接池初始化完成,最大并发连接数:3 ===
线程1尝试获取数据库连接...
线程1✅ 获取到数据库连接,开始执行SQL操作...
线程2尝试获取数据库连接...
线程2✅ 获取到数据库连接,开始执行SQL操作...
线程3尝试获取数据库连接...
线程3✅ 获取到数据库连接,开始执行SQL操作...
线程4尝试获取数据库连接...
线程1📝 SQL操作执行完成
线程1🔄 释放数据库连接,当前可用连接数:1
线程4✅ 获取到数据库连接,开始执行SQL操作...
线程5尝试获取数据库连接...
线程2📝 SQL操作执行完成
线程2🔄 释放数据库连接,当前可用连接数:1
线程5✅ 获取到数据库连接,开始执行SQL操作...
...(后续线程按同样逻辑执行)
二、CyclicBarrier:多线程协同的"循环屏障"
1. 核心功能
CyclicBarrier
(循环屏障)让多个线程相互等待,直到所有线程都到达某个"屏障点"后,再一起继续执行。与其他工具类不同,它支持重复使用(通过reset()
重置屏障)。
2. 适用场景
- 多阶段任务(如"需求分析→编码→测试"的开发流程)
- 数据分片处理(各线程处理完分片后,等待汇总结果)
- 模拟并发场景(所有线程就绪后同时执行)
3. 代码案例:多阶段开发流程
3个开发者需依次完成三个阶段任务,每个阶段必须全员完成后才能进入下一阶段:
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierDemo {public static void main(String[] args) {int developerCount = 3; // 3个开发者线程System.out.println("=== 项目启动,共" + developerCount + "名开发者参与 ===");// 屏障触发时执行的任务(可选,如阶段总结)CyclicBarrier barrier = new CyclicBarrier(developerCount, () -> {System.out.println("======================================");System.out.println("📢 所有开发者已完成当前阶段,准备进入下一阶段!");System.out.println("======================================");});for (int i = 0; i < developerCount; i++) {final int developerId = i + 1;new Thread(() -> {try {// 第一阶段:需求分析System.out.println("开发者" + developerId + ":开始需求分析(阶段1)");Thread.sleep((long) (Math.random() * 1000) + 500); // 模拟不同耗时System.out.println("开发者" + developerId + ":需求分析完成(阶段1),等待其他人...");barrier.await(); // 等待其他开发者// 第二阶段:编码System.out.println("开发者" + developerId + ":开始编码开发(阶段2)");Thread.sleep((long) (Math.random() * 1000) + 500);System.out.println("开发者" + developerId + ":编码开发完成(阶段2),等待其他人...");barrier.await();// 第三阶段:测试System.out.println("开发者" + developerId + ":开始功能测试(阶段3)");Thread.sleep((long) (Math.random() * 1000) + 500);System.out.println("开发者" + developerId + ":功能测试完成(阶段3),等待其他人...");barrier.await();System.out.println("开发者" + developerId + ":🎉 所有阶段任务完成!");} catch (Exception e) {e.printStackTrace();}}).start();}}
}
输出示例:
=== 项目启动,共3名开发者参与 ===
开发者1:开始需求分析(阶段1)
开发者2:开始需求分析(阶段1)
开发者3:开始需求分析(阶段1)
开发者2:需求分析完成(阶段1),等待其他人...
开发者1:需求分析完成(阶段1),等待其他人...
开发者3:需求分析完成(阶段1),等待其他人...
======================================
📢 所有开发者已完成当前阶段,准备进入下一阶段!
======================================
开发者1:开始编码开发(阶段2)
开发者2:开始编码开发(阶段2)
开发者3:开始编码开发(阶段2)
开发者3:编码开发完成(阶段2),等待其他人...
开发者1:编码开发完成(阶段2),等待其他人...
开发者2:编码开发完成(阶段2),等待其他人...
======================================
📢 所有开发者已完成当前阶段,准备进入下一阶段!
======================================
...(第三阶段流程类似)
三、CountDownLatch:等待前置任务的"倒计时器"
1. 核心功能
CountDownLatch
(倒计时器)让一个或多个线程等待其他线程完成指定操作后再执行。它维护一个计数器,线程完成任务后调用countDown()
递减计数器,当计数器为0时,所有等待的线程被唤醒。计数器不可重置,为一次性工具。
2. 适用场景
- 主线程等待子线程初始化完成(如服务器启动前的准备工作)
- 汇总多线程任务结果(如多个线程计算部分结果,主线程汇总)
- 模拟并发测试(等待所有线程就绪后同时执行)
3. 代码案例:服务器启动前的初始化
服务器启动前,需等待5个初始化任务(如加载配置、连接数据库)完成:
import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {int taskCount = 5; // 5个初始化任务CountDownLatch latch = new CountDownLatch(taskCount);System.out.println("=== 服务器启动流程开始,需完成" + taskCount + "项初始化任务 ===");// 启动初始化任务for (int i = 0; i < taskCount; i++) {final int taskId = i + 1;new Thread(() -> {try {System.out.println("初始化任务" + taskId + ":启动(当前剩余任务数:" + latch.getCount() + ")");// 模拟不同任务耗时(1-2秒)Thread.sleep((long) (Math.random() * 1000) + 1000);System.out.println("初始化任务" + taskId + ":执行成功");} catch (InterruptedException e) {e.printStackTrace();} finally {latch.countDown(); // 任务完成,计数器-1System.out.println("初始化任务" + taskId + ":已标记完成(剩余任务数:" + latch.getCount() + ")");}}).start();}// 主线程等待所有初始化完成System.out.println("=== 主线程等待所有初始化任务完成... ===");latch.await(); // 阻塞直到计数器为0System.out.println("=== 所有初始化任务完成,服务器启动成功!🎉 ===");}
}
输出示例:
=== 服务器启动流程开始,需完成5项初始化任务 ===
初始化任务1:启动(当前剩余任务数:5)
初始化任务2:启动(当前剩余任务数:5)
初始化任务3:启动(当前剩余任务数:5)
初始化任务4:启动(当前剩余任务数:5)
初始化任务5:启动(当前剩余任务数:5)
=== 主线程等待所有初始化任务完成... ===
初始化任务3:执行成功
初始化任务3:已标记完成(剩余任务数:4)
初始化任务1:执行成功
初始化任务1:已标记完成(剩余任务数:3)
初始化任务5:执行成功
初始化任务5:已标记完成(剩余任务数:2)
初始化任务2:执行成功
初始化任务2:已标记完成(剩余任务数:1)
初始化任务4:执行成功
初始化任务4:已标记完成(剩余任务数:0)
=== 所有初始化任务完成,服务器启动成功!🎉 ===
四、三者核心区别对比
特性 | Semaphore | CyclicBarrier | CountDownLatch |
---|---|---|---|
核心功能 | 控制并发访问的线程数量(限流) | 多线程相互等待,一起继续执行 | 等待其他线程完成后再执行 |
计数器/许可 | 许可数量(可动态调整) | 参与线程数(可通过reset() 重置) | 等待的任务数(一次性递减) |
阻塞对象 | 申请许可的线程(争夺资源的线程) | 所有参与的线程(相互等待) | 调用await() 的线程(等待者) |
复用性 | 可重复使用(许可释放后可再次获取) | 可重复使用(reset() 重置屏障) | 一次性(计数器到0后无法重置) |
典型场景 | 限流(连接池、并发请求控制) | 多阶段协同任务(分步骤流程) | 等待前置任务完成(初始化、汇总) |
核心方法 | acquire() /release() | await() /reset() | countDown() /await() |
五、总结
- Semaphore 是"门卫",通过许可机制控制同时访问资源的线程数,适合限流场景(如连接池、接口并发控制)。
- CyclicBarrier 是"集合点",让多个线程到达屏障后一起执行下一阶段,适合多步骤协同任务(如分阶段开发流程)。
- CountDownLatch 是"发令枪",等待所有前置任务完成后触发后续操作,适合初始化、结果汇总等场景。
这三个工具类均基于AQS(AbstractQueuedSynchronizer)实现,但设计目标不同。实际开发中需根据具体同步需求选择:如需控制并发量用Semaphore
,如需多线程协同用CyclicBarrier
,如需等待前置任务用CountDownLatch
。