并发 - JUC 三大辅助同步工具
并发编程 —— JUC 三大辅助同步工具
除了锁和原子类,JUC 还提供了一些非常有用的“同步辅助”工具,用于协调多个线程之间的复杂协作。其中最常用的有三个:CountDownLatch, CyclicBarrier, 和 Semaphore。
1. CountDownLatch (倒计时门闩)
-
核心思想: 允许一个或多个线程等待其他一组线程完成操作后再继续执行。它像一个倒数计数器。
-
工作流程:
- 创建一个
CountDownLatch并初始化一个计数值(比如N)。 - 一个或多个线程调用
latch.await()方法,这些线程会被阻塞,进入等待状态。 - 其他
N个线程执行各自的任务,每个任务完成后,调用一次latch.countDown()方法,将计数器减 1。 - 当计数器减到 0 时,所有在
await()方法上等待的线程都会被唤醒,继续执行。
- 创建一个
-
特点: 计数器只能使用一次,减到 0 后就不能再重置了。
-
底层实现:
- 基于 AQS (AbstractQueuedSynchronizer) 实现,利用了 AQS 的共享模式。
- 构造时传入的计数值
N被用作 AQS 的state状态。 await()方法会检查state是否为 0,如果不是,则线程进入 AQS 的等待队列并挂起。countDown()方法会以 CAS 的方式安全地将state减 1。当state被减到 0 时,它会唤醒所有在等待队列中的线程。
-
使用场景:
- 主线程等待子任务完成:例如,一个主线程需要启动多个服务(每个服务在一个独立的线程中启动),并且必须等待所有服务都成功启动后才能对外提供服务。
- 并行计算与结果汇总:将一个大任务拆分成多个子任务并行执行,主线程使用
await()等待所有子任务完成后,再对结果进行汇总和处理。 -
- 当你的场景是“一个线程(或少数几个线程)等待其他所有线程完成某个任务后,再继续执行”时。例如,等待多个初始化服务全部启动、等待多个文件下载完成、等待多个子任务计算完毕
-
生活比喻: 就像火箭发射的倒计时。
latch.await(): 总指挥官(主线程)下达了“等待”命令,他盯着倒计时屏幕,在倒数结束前绝不按下发射按钮。latch.countDown(): 每个负责检查燃料、电路、天气等岗位的工程师(子线程),完成自己的检查后,就向指挥中心报告“我已完成”,相当于让倒计时减 1。- 计数归零: 当所有工程师都报告完毕,倒计时变为 0,总指挥官立即按下发射按钮。
2. CyclicBarrier (循环栅栏)
-
核心思想: 让一组线程互相等待,直到所有线程都到达一个公共的“栅栏点”(Barrier Point),然后再一起继续执行。
-
工作流程:
- 创建一个
CyclicBarrier,并指定需要等待的线程数量(parties)。 - 每个线程在完成自己的一部分工作后,调用
barrier.await()方法,告诉栅栏“我到了”。 - 调用
await()的线程会被阻塞。 - 直到最后一个线程也调用了
await(),栅栏被“冲破”,所有被阻塞的线程会同时被唤醒,继续执行后续任务。
- 创建一个
-
特点: 可以循环使用。当所有线程被释放后,栅栏可以被重置,用于下一轮的等待。
-
底层实现:
- 基于 ReentrantLock 和 Condition 实现。
- 每个线程调用
await()时,会先获取锁,然后将一个计数器count减 1。 - 如果
count不为 0,则线程在Condition上await()进入等待状态。 - 如果
count为 0(表示最后一个线程已到达),该线程会唤醒Condition上所有等待的线程 (signalAll()),并重置栅栏以便下次使用。
-
使用场景:
- 多线程数据分片处理:在进行大规模数据计算时,可以将数据分成多片,每个线程处理一片。当所有线程都处理完当前阶段后(到达栅栏),再统一进入下一阶段(例如数据合并、结果交换)。
- 模拟并发测试:需要模拟大量用户在同一时刻发起请求的场景。可以让多个线程准备好请求数据,然后在栅栏处等待,当所有线程都准备好后,同时发起请求,以达到瞬时高并发的效果。
-
- 当你的场景是“一组线程需要达到一个共同的同步点,然后才能一起继续执行下一个阶段的任务”时。这种多发生在需要分阶段处理、且每个阶段所有参与者都必须同步的场景。例
如,多线程计算结果的阶段性汇总、模拟高并发测试的统一启动点。
- 当你的场景是“一组线程需要达到一个共同的同步点,然后才能一起继续执行下一个阶段的任务”时。这种多发生在需要分阶段处理、且每个阶段所有参与者都必须同步的场景。例
-
生活比喻: 就像组织一场春游。
- 约定好早上 8 点在公园门口(栅栏点)集合,人齐了才出发。
- 每个同学(线程)从自己家里出发,到达公园门口后,就调用
await(),表示“我到了,我开始等其他人”。 - 当最后一个同学也到达门口时,班长(栅栏)大喊一声“人齐了,出发!”,于是所有同学一起涌入公园开始游玩。
- 玩完第一个景点后,大家可以再约定到第二个景点门口集合,再次使用这个“栅栏”。
3. Semaphore (信号量)
-
核心思想: 用于控制同时访问某个特定资源的线程数量。它内部维护了一组“许可证”。
-
工作流程:
- 创建一个
Semaphore并初始化许可证的数量。 - 线程在访问资源前,必须先调用
semaphore.acquire()方法获取一个许可证。如果许可证已发完,线程就会被阻塞。 - 线程访问完资源后,必须调用
semaphore.release()方法归还许可证。
- 创建一个
-
特点: 主要用于流量控制和资源限制。
-
底层实现:
- 同样基于 AQS 实现,许可证的数量就是 AQS 的
state。 acquire()方法会尝试以 CAS 方式将state减 1。如果state已经为 0,则获取失败,线程进入 AQS 等待队列。release()方法会将state加 1,并唤醒等待队列中的一个线程。Semaphore也支持公平和非公平两种模式。
- 同样基于 AQS 实现,许可证的数量就是 AQS 的
-
使用场景:
- 资源池管理:例如数据库连接池。如果连接池中只有 10 个连接,就可以创建一个
Semaphore(10),每个线程在获取连接前先acquire(),用完后release(),从而保证最多只有 10 个线程能同时持有连接。 - 接口流量控制:限制对某个第三方 API 或服务的并发调用次数,以防止因请求过多而被封禁或产生高额费用。
- 控制内存密集型操作:如果某个操作非常消耗内存,可以通过信号量限制同时执行该操作的线程数,防止系统内存溢出。
- 资源池管理:例如数据库连接池。如果连接池中只有 10 个连接,就可以创建一个
-
生活比喻: 就像一个停车场的入口。
Semaphore(3): 表示这个停车场总共只有 3 个车位(许可证)。acquire(): 每当有车想进入停车场,就必须在入口的闸机处取一张停车卡(获取许可证)。如果卡发完了,后面的车就只能在入口外排队等着。release(): 每当有车离开停车场,它会把停车卡还给闸机(归还许可证),这样排队的下一辆车就可以进来了。
总结对比
| 工具 | 核心作用 | 是否可重用 | 底层基础 | 计数器 | 生活比喻 |
|---|---|---|---|---|---|
CountDownLatch |
一个或多个线程等其他线程 | 否 | AQS (共享) | countDown() 减计数 | 火箭发射倒计时 |
CyclicBarrier |
一组线程互相等 | 是 | Lock & Condition | await() 减计数 | 春游集合等人 |
Semaphore |
控制并发数量 | 是 | AQS | 停车场车位 |

浙公网安备 33010602011771号