1. JUC并发工具概述
Java并发编程一直是开发者必须掌握的核心技能之一。在Java 5之前,我们只能通过synchronized关键字和wait/notify机制来实现线程同步,这种方式不仅代码冗长,而且容易出错。2004年发布的Java 5引入了一个革命性的并发工具包——java.util.concurrent(简称JUC),它提供了一系列强大而灵活的并发构建块,彻底改变了Java并发编程的面貌。
JUC包中的工具大致可以分为以下几类:
- 原子变量类(Atomic):如AtomicInteger、AtomicReference等
- 锁机制(Locks):如ReentrantLock、ReadWriteLock等
- 并发集合(Collections):如ConcurrentHashMap、CopyOnWriteArrayList等
- 线程池(Executor):如ThreadPoolExecutor、ScheduledThreadPool等
- 同步工具(Synchronizers):如CountDownLatch、CyclicBarrier、Semaphore等
这些工具不仅性能优异,而且提供了更高级的抽象,让我们能够以更声明式的方式编写并发代码。本文将重点介绍JUC中最常用的几种同步工具:CountDownLatch、CyclicBarrier和Semaphore,它们虽然功能相似,但适用场景却各有不同。
2. CountDownLatch:一次性门闩
2.1 核心概念与使用场景
CountDownLatch可以理解为一个倒计时门闩,它允许一个或多个线程等待其他线程完成操作。它的典型使用场景包括:
- 主线程等待多个子线程完成初始化
- 多个线程等待某个外部条件就绪
- 模拟并发测试时等待所有线程就位
java复制// 典型用法示例
CountDownLatch latch = new CountDownLatch(N); // 初始化计数器
// 工作线程
new Thread(() -> {
doWork();
latch.countDown(); // 计数器减1
}).start();
// 主线程
latch.await(); // 阻塞直到计数器归零
2.2 实现原理深度解析
CountDownLatch的内部实现基于AQS(AbstractQueuedSynchronizer),这是JUC中大多数同步器的基础框架。AQS通过一个volatile int类型的state变量来表示同步状态,并通过一个FIFO队列来管理等待线程。
当调用countDown()时,实际上是通过CAS操作将state值减1。当state变为0时,会唤醒所有在await()上阻塞的线程。值得注意的是,CountDownLatch的计数器是一次性的,一旦归零就无法重置,这也是它与CyclicBarrier的主要区别之一。
提示:虽然CountDownLatch的计数器不能重置,但可以通过创建新的实例来"复用"。不过这种场景下可能更适合使用CyclicBarrier。
2.3 实战案例:并行任务聚合
假设我们需要从多个数据源并行加载数据,等所有数据都加载完成后再进行聚合处理:
java复制public class DataLoader {
private static final int SOURCE_COUNT = 3;
public Map<String, Object> loadAllData() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(SOURCE_COUNT);
Map<String, Object> result = new ConcurrentHashMap<>();
// 并行加载三个数据源
new Thread(() -> {
result.put("source1", loadFromSource1());
latch.countDown();
}).start();
new Thread(() -> {
result.put("source2", loadFromSource2());
latch.countDown();
}).start();
new Thread(() -> {
result.put("source3", loadFromSource3());
latch.countDown();
}).start();
latch.await(); // 等待所有数据加载完成
return result;
}
}
在实际项目中,我经常使用CountDownLatch来实现服务的并行初始化。比如在系统启动时,需要同时初始化缓存、连接池、配置文件等,使用CountDownLatch可以显著缩短启动时间。
3. CyclicBarrier:可循环使用的屏障
3.1 核心概念与使用场景
CyclicBarrier可以理解为循环屏障,它让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行。与CountDownLatch不同,CyclicBarrier的计数器可以重置后重复使用。
典型使用场景包括:
- 多阶段任务,每个阶段需要所有线程都完成才能进入下一阶段
- 并行计算,需要等待所有子任务完成才能合并结果
- 模拟测试中的多线程协调
java复制// 典型用法示例
CyclicBarrier barrier = new CyclicBarrier(N, () -> {
// 所有线程到达屏障后执行的回调
});
// 工作线程
new Thread(() -> {
doPhase1Work();
barrier.await(); // 等待其他线程
doPhase2Work();
}).start();
3.2 实现原理与关键细节
CyclicBarrier同样基于AQS实现,但它的状态管理更为复杂。内部使用了一个Generation对象来表示当前代(generation),当所有线程都到达屏障或屏障被破坏时,就会创建一个新的Generation。
await()方法的实现逻辑大致如下:
- 获取当前代的锁
- 递减计数器
- 如果计数器不为零,线程进入等待
- 如果计数器为零,执行屏障动作并唤醒所有线程
- 释放锁
CyclicBarrier的一个关键特性是它可以自动重置,这是通过创建新的Generation对象实现的。此外,如果某个线程在等待时被中断或超时,屏障会被破坏(broken),所有等待中的线程会收到BrokenBarrierException。
3.3 实战案例:多阶段并行计算
假设我们需要对一个大型数据集进行多阶段处理,每个阶段都需要所有工作线程完成当前阶段才能进入下一阶段:
java复制public class MultiStageProcessor {
private static final int THREAD_COUNT = 4;
private static final int PHASE_COUNT = 3;
public void process(DataSet data) {
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("所有线程完成当前阶段");
});
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int phase = 0; phase < PHASE_COUNT; phase++) {
processPhase(data, phase);
barrier.await();
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
private void processPhase(DataSet data, int phase) {
// 阶段处理逻辑
}
}
在实际项目中,CyclicBarrier特别适合处理需要多轮协调的并行算法。我曾经在一个图像处理项目中使用它来实现分块处理-合并-再处理的流水线模式,效果非常好。
4. Semaphore:控制并发数量的信号量
4.1 核心概念与使用场景
Semaphore(信号量)是用来控制同时访问特定资源的线程数量的同步工具。它维护了一组许可证(permits),线程在访问资源前必须先获取许可证,使用完后释放。
典型使用场景包括:
- 资源池管理(如数据库连接池)
- 限流控制
- 有界集合的生产者-消费者问题
java复制// 典型用法示例
Semaphore semaphore = new Semaphore(N); // N个许可证
// 工作线程
semaphore.acquire(); // 获取许可证
try {
useResource();
} finally {
semaphore.release(); // 释放许可证
}
4.2 实现原理与高级用法
Semaphore同样基于AQS实现,它的state变量表示当前可用的许可证数量。acquire()方法会减少许可证数量,如果不足则线程进入等待;release()方法会增加许可证数量,并可能唤醒等待线程。
Semaphore有两种模式:
- 公平模式:按照FIFO顺序分配许可证
- 非公平模式:允许插队,吞吐量更高
java复制// 创建公平信号量
Semaphore fairSemaphore = new Semaphore(N, true);
Semaphore还提供了一些有用的方法:
- tryAcquire():尝试获取许可证,不阻塞
- acquireUninterruptibly():不可中断的获取
- drainPermits():获取所有可用许可证
- reducePermits():减少许可证总数(动态调整)
4.3 实战案例:资源池管理
下面是一个简单的数据库连接池实现:
java复制public class ConnectionPool {
private final List<Connection> pool;
private final Semaphore semaphore;
public ConnectionPool(int size) {
this.pool = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
pool.add(createConnection());
}
this.semaphore = new Semaphore(size);
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire();
synchronized (pool) {
return pool.remove(0);
}
}
public void releaseConnection(Connection conn) {
synchronized (pool) {
pool.add(conn);
}
semaphore.release();
}
private Connection createConnection() {
// 创建真实连接
return null;
}
}
在实际项目中,Semaphore是限流和资源管理的利器。我曾经用它来实现一个API调用限流器,确保系统不会因为外部调用过多而崩溃。需要注意的是,Semaphore只控制数量不控制资源本身,资源的管理仍需开发者自己实现。
5. 三种同步工具的对比与选型
5.1 功能对比
| 特性 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 重用性 | 一次性 | 可循环使用 | 可循环使用 |
| 计数器方向 | 递减 | 递增到固定值 | 增减 |
| 阻塞方 | 主线程等待工作线程 | 工作线程相互等待 | 获取不到许可时阻塞 |
| 典型用途 | 启动信号 | 多阶段任务同步 | 资源访问控制 |
| 是否可中断 | 是 | 是 | 是 |
| 回调支持 | 无 | 有 | 无 |
5.2 选型指南
选择哪种同步工具取决于具体的场景需求:
- 当需要一个线程等待其他线程完成某些操作时,选择CountDownLatch
- 当需要一组线程相互等待以达到共同执行点时,选择CyclicBarrier
- 当需要控制对资源的并发访问数量时,选择Semaphore
在实际项目中,这三种工具经常组合使用。比如在一个分布式任务调度系统中:
- 使用CountDownLatch等待所有worker节点启动完成
- 使用CyclicBarrier协调各节点的分阶段计算
- 使用Semaphore控制对共享存储的并发写入
5.3 性能考量与最佳实践
虽然JUC工具性能已经非常优秀,但在高并发场景下仍需注意:
- 尽量减少同步区域:只在必要的时候获取锁或信号量
- 合理设置超时:避免死锁,增加系统健壮性
- 注意异常处理:特别是InterruptedException
- 避免过度同步:能用无锁方案优先使用无锁
- 监控工具状态:特别是CyclicBarrier的broken状态
java复制// 最佳实践示例:带超时和异常处理的同步代码
try {
if (!barrier.await(1, TimeUnit.SECONDS)) {
// 超时处理
}
} catch (BrokenBarrierException e) {
// 屏障被破坏处理
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
}
6. 常见问题与调试技巧
6.1 死锁与活锁问题
在使用这些同步工具时,最常见的陷阱就是死锁和活锁。我曾经遇到一个生产问题:两个线程互相等待对方释放资源,同时又在CyclicBarrier上等待,形成了复杂的死锁。
调试这类问题的技巧包括:
- 使用jstack获取线程转储,分析线程阻塞点
- 添加详细的日志,记录同步点的进入和退出
- 使用可视化工具(如JConsole)监控线程状态
- 为同步操作设置合理的超时时间
6.2 性能瓶颈定位
高并发场景下,同步工具可能成为性能瓶颈。通过以下方法可以定位问题:
- 使用JProfiler等工具分析锁竞争情况
- 监控AQS队列长度
- 考虑使用更轻量级的同步方案(如CAS)
- 调整并发级别(如Semaphore的许可数量)
6.3 内存一致性保证
JUC工具通过happens-before关系提供内存可见性保证,但开发者仍需注意:
- 共享变量的修改应该在获取锁/信号量之前完成
- 屏障前后的操作要注意内存可见性
- 避免在同步块内进行耗时操作
java复制// 正确的内存可见性示例
class CorrectExample {
private int sharedState;
private final Lock lock = new ReentrantLock();
void updateState() {
// 修改共享变量
int newState = computeNewState();
lock.lock();
try {
sharedState = newState;
} finally {
lock.unlock();
}
}
}
7. 高级应用与模式扩展
7.1 组合同步模式
在实际项目中,我们经常需要组合使用多种同步工具。例如,实现一个并行任务框架:
java复制public class ParallelTaskRunner {
private final Executor executor;
private final Semaphore taskSemaphore;
private final CountDownLatch completionLatch;
public ParallelTaskRunner(int concurrency, int taskCount) {
this.executor = Executors.newFixedThreadPool(concurrency);
this.taskSemaphore = new Semaphore(concurrency);
this.completionLatch = new CountDownLatch(taskCount);
}
public void submitTask(Runnable task) {
executor.execute(() -> {
try {
taskSemaphore.acquire();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
taskSemaphore.release();
completionLatch.countDown();
}
});
}
public void awaitCompletion() throws InterruptedException {
completionLatch.await();
}
}
7.2 自定义同步器
虽然JUC提供了丰富的同步工具,但有时我们需要自定义同步器。基于AQS实现自定义同步器的基本步骤:
- 继承AbstractQueuedSynchronizer
- 实现tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)
- 提供友好的API封装
我曾经实现过一个"温度计"同步器,只有当足够多的线程到达时才放行,类似于CyclicBarrier但阈值可以动态调整。
7.3 响应式编程中的同步
在现代响应式编程中(如Reactor、RxJava),虽然直接使用同步工具的情况减少了,但理解这些底层机制仍然很重要。响应式操作符如zip、merge实际上就是在更高层次上实现了类似的同步模式。
例如,Project Reactor中的Mono.when()就类似于CountDownLatch,而Flux.buffer()的分组功能与CyclicBarrier有异曲同工之妙。理解JUC同步工具的底层原理,有助于更好地使用这些高级抽象。
