JUC 并发协作工具全过程
java.util.concurrent 不只是线程池。它把“等待一组任务结束、限制并发、多个线程阶段汇合、动态注册参与者、两个线程交换数据”等协作协议封装成可验证的工具。本章以 JDK 7/8 为主,既讲 API,也讲状态如何变化、线程为何阻塞、谁负责唤醒、失败如何传播,以及错误使用为何会永久卡住生产线程。
学习目标
- 根据业务语义选择
CountDownLatch、Semaphore、CyclicBarrier、Phaser、Exchanger,而不是背 API; - 理解 AQS 共享模式如何支撑 Latch 与 Semaphore;
- 理解 CyclicBarrier 的
count、Generation、Condition 和 broken 状态; - 正确处理超时、中断、许可泄漏、屏障损坏和动态参与者注销;
- 能构造故障并从
jstack、指标和业务日志中定位等待原因; - 区分线程协作工具、线程池并发度、连接池容量和分布式限流。
一、先按“谁等谁”选择
flowchart TD
A["出现线程协作需求"] --> B{"等待事件还是限制资源"}
B -- "等待N个事件完成" --> C["CountDownLatch"]
B -- "限制并发许可证" --> D["Semaphore"]
B -- "固定线程分阶段汇合" --> E["CyclicBarrier"]
B -- "参与者动态变化和多阶段" --> F["Phaser"]
B -- "两个线程交换数据" --> G["Exchanger"]| 工具 | 核心状态 | 谁使状态变化 | 是否复用 | 典型语义 |
|---|---|---|---|---|
CountDownLatch | 剩余计数 | 完成者 countDown | 不可重置 | 一个/多个线程等待 N 个事件 |
Semaphore | 可用许可数 | 获取者扣减,持有者归还 | 持续复用 | 同时最多 N 个操作 |
CyclicBarrier | 本代未到达数 | 参与者 await | 可按代复用 | 固定 N 个线程互相等待 |
Phaser | phase、注册数、未到达数 | 动态参与者注册/到达/注销 | 多阶段 | 动态多阶段工作流 |
Exchanger | 两个线程的交换槽 | 两个参与者 exchange | 可复用 | 双方在汇合点交换对象 |
它们都是单 JVM 内线程协作工具,不会跨应用实例同步。分布式部署中,每个实例各有自己的 Latch/Semaphore,不能拿它们实现全局限流或分布式屏障。
二、阻塞与唤醒的共同基础
线程等待不是靠不停轮询:
flowchart TD
A["线程检查条件"] --> B{"条件满足吗"}
B -- "是" --> C["继续执行"]
B -- "否" --> D["进入等待队列"]
D --> E["LockSupport.park挂起"]
F["其他线程改变状态"] --> G["选择等待节点"]
G --> H["LockSupport.unpark唤醒"]
H --> Apark/unpark 是底层阻塞原语之一;Latch、Semaphore 通常通过 AQS 的共享队列实现,CyclicBarrier 使用 ReentrantLock + Condition。调用者不应依赖内部队列的具体节点布局,但要理解:等待必须有状态条件,状态变化后必须有唤醒,醒来后仍要重新检查条件。
更完整的 AQS 队列、CAS、共享传播原理见 AQS 与 JUC 工具类全过程。
三、CountDownLatch:等待 N 个事件
3.1 不是“等待 N 个线程”
构造参数存入 AQS 的 state,它表示 N 个事件,不强制对应 N 个线程。同一线程可完成多个事件,也可由线程池复用线程完成。
CountDownLatch latch = new CountDownLatch(3);await():当state > 0,当前线程进入 AQS 共享等待;countDown():CAS 把state减一,但不会降到负数;- 从 1 减到 0 的线程触发共享释放,传播唤醒所有等待者;
state == 0后调用await()立即返回;- 归零后不能恢复,额外
countDown()没有效果。
3.2 状态与唤醒流程
flowchart TD
A["创建Latch state=N"] --> B["等待线程调用await"]
B --> C{"state等于0吗"}
C -- "是" --> D["立即通过"]
C -- "否" --> E["进入AQS共享等待队列"]
F["工作任务调用countDown"] --> G["CAS让state减1"]
G --> H{"减到0吗"}
H -- "否" --> I["等待者继续等待"]
H -- "是" --> J["共享释放并传播唤醒"]
J --> D3.3 JDK 7 可运行 Demo:服务启动门闩
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class StartupLatchDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(3);
final CountDownLatch ready = new CountDownLatch(3);
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(3);
try {
for (int i = 1; i <= 3; i++) {
final int workerId = i;
pool.execute(new Runnable() {
public void run() {
ready.countDown();
try {
start.await();
System.out.println("worker-" + workerId + " started");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown();
}
}
});
}
if (!ready.await(3, TimeUnit.SECONDS)) {
throw new IllegalStateException("workers not ready");
}
start.countDown();
if (!done.await(5, TimeUnit.SECONDS)) {
throw new IllegalStateException("workers timeout");
}
} finally {
pool.shutdownNow();
}
}
}这里三个 Latch 分别表达准备完成、统一开闸、全部结束,语义不能混成一个。done.countDown() 放在 finally,保证任务失败也报告“这个任务已经结束”;但结束不等于成功,真实系统还要单独收集失败原因。
3.4 内存可见性
规范保证:某线程在 countDown() 前的操作,happens-before 另一个线程从对应 await() 成功返回后的操作。因此工作线程先写结果再 countDown,汇总线程 await 返回后可看到结果;共享结果容器本身仍需正确处理多个工作线程并发写入。
3.5 典型错误
| 错误 | 为什么会出问题 | 修复 |
|---|---|---|
| 初始计数和实际任务数不一致 | 过大永久等,过小提前放行 | 任务列表确定后创建,或改用 Phaser |
countDown 不在 finally | 异常路径漏减 | finally 中减计数并另存错误 |
无超时 await() | 一个任务挂死拖死主流程 | 使用业务 deadline |
| 线程池小于等待结构需要 | 任务可能尚未得到执行线程 | 检查线程池拓扑,避免同池互等 |
| 想重复使用同一个 Latch | 归零后不会重置 | 每轮新建或使用 CyclicBarrier/Phaser |
四、Semaphore:许可证不是线程数
Semaphore 的 AQS state 表示当前可用许可数。acquire(n) 尝试让 state 减 n;不足时进入共享等待。release(n) 让 state 加 n 并唤醒可能满足条件的等待者。
4.1 许可没有所有权
Semaphore 不记录“哪个线程持有哪张票”。任意线程都能 release(),甚至没 acquire 也能 release,许可数就会被错误放大。这和 ReentrantLock 必须由持有线程解锁不同。
4.2 正确获取与释放模板
boolean acquired = false;
try {
acquired = semaphore.tryAcquire(300, java.util.concurrent.TimeUnit.MILLISECONDS);
if (!acquired) {
throw new IllegalStateException("downstream busy");
}
callDownstream();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (acquired) {
semaphore.release();
}
}原页面直接在 finally 中无条件 release() 是危险示例:若 acquire() 等待时被中断,它没有拿到许可,却多释放一次,长期会突破并发上限。本次已修正。
4.3 获取流程
flowchart TD
A["调用acquire或tryAcquire"] --> B["读取availablePermits"]
B --> C["计算remaining=available-request"]
C --> D{"remaining小于0吗"}
D -- "否" --> E{"CAS扣减成功吗"}
E -- "否" --> B
E -- "是" --> F["获得许可执行业务"]
D -- "是" --> G["排队或立即返回失败"]
F --> H["finally中release"]
H --> I["增加许可并唤醒等待者"]4.4 公平与非公平
Semaphore nonfair = new Semaphore(20); // 默认非公平
Semaphore fair = new Semaphore(20, true); // 公平- 非公平模式允许新线程在队列前尝试抢许可,吞吐通常更好,但老线程可能等待更久;
- 公平模式通常按等待先后获取,尾延迟更可预测,但维护排队和切换有成本;
- 公平 Semaphore 的无参
tryAcquire()仍可能插队,若必须尊重公平顺序,应使用带超时的tryAcquire(0, unit)等符合 API 语义的方式并验证版本文档。
4.5 多许可与饥饿
acquire(5) 要一次拿到 5 个许可。大量只需 1 个许可的任务与大请求混合时,调度和公平性更复杂,大请求可能长期无法满足。不要把不同成本任务随意混用一个 Semaphore;可按资源类型拆分,或用加权限流器。
4.6 商业 Demo:保护下游
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public final class PaymentGatewayGuard {
private final Semaphore permits = new Semaphore(30);
public String query(String orderNo) throws Exception {
boolean acquired = permits.tryAcquire(100, TimeUnit.MILLISECONDS);
if (!acquired) {
throw new IllegalStateException("payment gateway overloaded");
}
try {
return callPaymentWithTimeout(orderNo);
} finally {
permits.release();
}
}
private String callPaymentWithTimeout(String orderNo) {
return "SUCCESS:" + orderNo;
}
}Semaphore 只限制进入代码段的并发,不限制单位时间 QPS。例如 30 个许可、每次 10ms,理论吞吐和每次 2s 完全不同。QPS 限流应使用令牌桶/漏桶;全局多实例限流需网关或分布式方案。
4.7 与线程池、连接池的关系
若线程池 200、Semaphore 100、数据库连接池 20,数据库仍只有约 20 个连接能力,其余线程会等待连接。并发上限要从最小下游容量反推,避免在 JVM 内制造多层队列。
五、CyclicBarrier:固定参与者的分代屏障
CyclicBarrier(parties) 要求固定数量参与者调用 await()。每到一个线程,当前代 count 减一;最后到达者执行可选 barrierAction,创建下一代并唤醒本代所有线程。
5.1 为什么叫 Cyclic
flowchart TD
A["第0代 count=N"] --> B["参与者陆续await"]
B --> C["最后到达者使count归0"]
C --> D["执行barrierAction"]
D --> E["创建新Generation"]
E --> F["count重置为N"]
F --> G["第1代可以再次await"]每轮称为一代 Generation。重置 count 不等于复用同一批状态;新 Generation 用于区分上一代正常完成、损坏或被重置。
5.2 底层为何不是 AQS
CyclicBarrier 需要所有参与者等待一个复杂条件,并在中断/超时时让整代进入 broken 状态。JDK 7/8 使用 ReentrantLock 保护 count 和 generation,使用 Condition 的 await/signalAll 等待与唤醒。
5.3 barrierAction 谁执行
最后一个到达 await() 的参与线程执行 barrierAction。它执行期间仍处于屏障推进的关键路径:动作太慢或阻塞,其他参与者无法越过屏障;动作抛异常会使本代损坏。因此这里只做快速、确定的汇总或状态切换,不做无超时远程调用。
5.4 一个线程失败为什么影响所有人
flowchart TD
A["多个线程等待同一代"] --> B{"某线程中断超时或reset"}
B -- "否" --> C["最后到达后整代通过"]
B -- "是" --> D["breakBarrier标记broken"]
D --> E["signalAll唤醒所有等待者"]
E --> F["其他线程抛BrokenBarrierException"]屏障代表“大家都到齐才继续”的共同承诺。一人无法履约,其他人继续执行会破坏阶段一致性,所以整代失败。超时线程得到 TimeoutException,被中断线程得到 InterruptedException,其他等待者通常得到 BrokenBarrierException。
5.5 JDK 7 Demo:两阶段对账
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ReconcileBarrierDemo {
public static void main(String[] args) throws Exception {
final int parties = 3;
final CyclicBarrier barrier = new CyclicBarrier(parties, new Runnable() {
public void run() {
System.out.println("all shards completed current phase");
}
});
ExecutorService pool = Executors.newFixedThreadPool(parties);
try {
for (int i = 0; i < parties; i++) {
final int shard = i;
pool.execute(new Runnable() {
public void run() {
try {
loadShard(shard);
barrier.await(2, TimeUnit.SECONDS);
compareShard(shard);
barrier.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
System.err.println("shard " + shard + " failed: " + e);
}
}
});
}
} finally {
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
pool.shutdownNow();
}
}
private static void loadShard(int shard) { }
private static void compareShard(int shard) { }
}参与者数量必须与真正会调用 await 的任务数匹配。线程池若只有 2 个线程,却提交 3 个屏障参与者,前两个线程在 await 占住线程,第三个任务留在队列,屏障永远无法凑齐——这是资源饥饿,不是 CyclicBarrier 自己失效。
5.6 reset 不是普通恢复按钮
reset() 会打破当前代、唤醒等待者并创建新代。当前等待线程收到异常,业务必须明确放弃旧轮次,不能在未知状态下直接重试。需要频繁动态增减参与者时,更适合 Phaser。
六、CountDownLatch 与 CyclicBarrier 深度比较
| 维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 关系 | 等待者等完成事件 | 参与者互相等待 |
| 状态改变 | countDown() 不阻塞 | await() 既到达又等待 |
| 到零后 | 永久开放 | 推进到下一代并重置 |
| 失败传播 | 某工作失败不会自动通知其他等待者 | 一位等待者中断/超时可打破整代 |
| 参与者 | 事件数固定,但完成线程可不同 | 固定 parties 个 await 调用者 |
| 回调 | 无内建归零回调 | 最后到达者执行 barrierAction |
| 底层 | AQS 共享模式 | ReentrantLock + Condition + Generation |
一句话:主线程等一批独立任务完成用 Latch;固定参与者分阶段、每阶段必须全部到齐用 Barrier。
七、Phaser:动态参与者与多阶段
Phaser 从 JDK 7 提供,解决 CyclicBarrier 参与者固定、复杂多阶段编排不灵活的问题。核心概念:
phase:当前阶段编号,从 0 递增;registered parties:已注册参与者数;unarrived parties:本阶段尚未到达数;- terminated:终止后等待立即返回负值。
7.1 API 语义
| API | 注册数 | 未到达数 | 当前线程是否等待 |
|---|---|---|---|
register() | +1 | +1 | 否 |
bulkRegister(n) | +n | +n | 否 |
arrive() | 不变 | -1 | 否 |
arriveAndAwaitAdvance() | 不变 | -1 | 是 |
arriveAndDeregister() | -1 | -1 | 否 |
awaitAdvance(phase) | 不变 | 不变 | 等待指定 phase 变化 |
7.2 动态批处理 Demo
import java.util.concurrent.Phaser;
public class PhaserDemo {
public static void main(String[] args) throws Exception {
final Phaser phaser = new Phaser(1); // 主协调者先注册
for (int i = 0; i < 3; i++) {
final int worker = i;
phaser.register();
new Thread(new Runnable() {
public void run() {
try {
System.out.println("load-" + worker);
phaser.arriveAndAwaitAdvance();
System.out.println("check-" + worker);
} finally {
phaser.arriveAndDeregister();
}
}
}, "phaser-" + i).start();
}
phaser.arriveAndDeregister(); // 主协调者完成注册并退出
while (!phaser.isTerminated()) {
Thread.sleep(10L);
}
}
}为什么先注册再启动线程:若线程先启动后才注册,它可能在注册发生前到达阶段,造成阶段提前推进。finally 注销避免失败线程永远占据注册名额。
7.3 onAdvance 扩展点
继承 Phaser 可重写 onAdvance(int phase, int registeredParties) 决定是否终止。默认在注册参与者变为 0 时终止。回调应短小无阻塞;若要记录每阶段结果,可以在外部维护线程安全状态。
7.4 分层 Phaser
参与者非常多时,可建立父子 Phaser,把竞争分散到多个子节点,再由子节点向父节点汇报阶段推进。它适合单 JVM 大规模分阶段算法;普通业务批处理不要为了“高级”而引入复杂树形 Phaser。
八、Exchanger:两个线程交换对象
第一个线程调用 exchange(A) 后等待;第二个线程调用 exchange(B),双方分别得到对方对象。它交换的是对象引用,不会复制对象,之后双方同时修改该可变对象仍可能数据竞争。
flowchart TD
A["线程A携带对象A到交换点"] --> C["等待配对"]
B["线程B携带对象B到交换点"] --> C
C --> D["线程A得到对象B"]
C --> E["线程B得到对象A"]import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
public class ExchangerDemo {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<String>();
new Thread(new Runnable() {
public void run() {
try {
String received = exchanger.exchange("bank-data", 1, TimeUnit.SECONDS);
System.out.println("A received " + received);
} catch (Exception e) {
System.err.println("A exchange failed: " + e);
}
}
}, "reconcile-A").start();
new Thread(new Runnable() {
public void run() {
try {
String received = exchanger.exchange("platform-data", 1, TimeUnit.SECONDS);
System.out.println("B received " + received);
} catch (Exception e) {
System.err.println("B exchange failed: " + e);
}
}
}, "reconcile-B").start();
}
}Exchanger 只适合成对交换。参与线程为奇数、某一方异常退出或配对错位时,另一方会一直等,所以生产必须有超时。普通生产者消费者更适合 BlockingQueue。
九、商业场景选型
9.1 启动初始化
应用需要等待字典、规则、配置加载完成再接流量,可用 CountDownLatch;每项初始化要记录成功/失败,await 要有超时。不要在 Spring 主线程无限等待网络,否则发布系统只能看到“启动卡死”。
9.2 保护第三方接口
Semaphore 限制本实例同时调用数,同时配置 HTTP 连接/读取超时和连接池上限。多实例总并发是“每实例许可 × 实例数”,扩容会放大流量,必须结合网关或下游配额。
9.3 分片对账
固定分片、每阶段必须全部完成可用 CyclicBarrier;参与者会动态增减或阶段不同则用 Phaser。多数业务批处理更适合任务状态表、调度平台或消息队列,因为它们能跨进程恢复,而 JVM 屏障随进程重启消失。
9.4 双缓冲交换
一个线程填充缓冲区、另一个线程消费,理论上可用 Exchanger 交换满/空缓冲;现代业务更常用有界 BlockingQueue,因为它支持多个生产者消费者并表达背压。
十、生产故障排查 Runbook
flowchart TD
A["线程长时间等待告警"] --> B["取得多份jstack"]
B --> C{"栈顶等待哪个工具"}
C -- "CountDownLatch.await" --> D["核对剩余计数和未结束任务"]
C -- "Semaphore.acquire" --> E["核对许可泄漏和持有时长"]
C -- "CyclicBarrier.await" --> F["核对parties线程池和broken"]
C -- "Phaser等待" --> G["核对注册未到达和注销"]
C -- "Exchanger.exchange" --> H["核对配对线程和超时"]10.1 通用证据
- 固定故障时间、接口、批次号和线程池名;
- 连续获取至少两到三份线程栈,判断是瞬时等待还是长期不变;
- 查看线程池 active、queue、completed、reject 和最老任务年龄;
- 记录工具关键状态:Latch count、Semaphore available/queue length、Barrier waiting/broken、Phaser phase/registered/unarrived;
- 找“本应改变状态”的线程为什么没运行、失败或卡在 I/O;
- 先用超时、限流、降级止损,再修复计数、生命周期或线程池拓扑。
10.2 Latch 永久等待
jstack 只看到主线程在 CountDownLatch.await 还不够。用批次任务表或日志核对哪些任务没有到 finally;检查任务是否被拒绝、仍在队列、线程池是否关闭、是否卡在无超时 I/O。若 count 无法映射到具体任务,说明可观测性设计不足,应给每个事件独立 ID 和最终状态。
10.3 Semaphore 许可越来越多或越来越少
- 越来越少:成功 acquire 后有异常路径漏 release,或任务长期不结束;
- 越来越多:未 acquire 就 release、重复 release;
- 许可正常但仍慢:受限代码内部依赖变慢,持有时间上升导致吞吐下降。
监控不能只看 availablePermits(),还要看等待线程数、获取超时数、许可持有时长和受保护下游 P99。
10.4 Barrier 一直差一个参与者
检查 parties 是否与真实任务数一致、线程池是否有足够线程让所有参与任务同时到达、某参与者是否提前异常、是否忘了调用 await。若已 broken,所有参与者应终止当前轮次并统一补偿,不能部分线程继续下一阶段。
10.5 Phaser 无法推进
比较 registered 与 unarrived,检查动态注册后是否必然 arrive/deregister。最常见错误是任务失败后没有注销,或主协调者注册后忘记退出。记录每个参与者的注册、到达和注销事件才能定位缺口。
十一、JDK 7、JDK 8 与后续版本
| 能力 | JDK 7 | JDK 8 | 现代 JDK |
|---|---|---|---|
| CountDownLatch/Semaphore/CyclicBarrier/Exchanger | 可用 | 可用 | 语义持续兼容 |
| Phaser | JDK 7 已有 | 可用 | 持续兼容 |
| Lambda Demo | 不支持,匿名内部类 | 支持 | 支持 |
| CompletableFuture | 无 | 引入 | 后续增加超时等 API |
| 虚拟线程 | 无 | 无 | JDK 21 正式提供 |
虚拟线程降低阻塞线程的资源成本,但不会补上漏掉的 countDown/release/deregister,也不会让错误 parties 自动匹配。协作协议错误在任何线程模型下都仍会卡住。
十二、面试标准回答
CountDownLatch 原理是什么
它用 AQS state 表示剩余事件数。await 在 state 非零时进入共享等待队列,countDown CAS 减一;从 1 减到 0 的线程触发共享释放,传播唤醒等待者。归零后永久开放,不能重置。
Semaphore 原理是什么
Semaphore 用 AQS state 表示可用许可。acquire CAS 扣减,许可不足则共享排队;release 增加许可并唤醒等待者。许可没有线程所有权,因此未获取就 release 会人为放大并发上限。
CountDownLatch 和 CyclicBarrier 区别
Latch 是等待者等 N 个事件,由完成者 countDown,归零后不可复用;Barrier 是固定参与者通过 await 互相等待,最后到达者推进 Generation,正常情况下可按代复用。Barrier 任一等待者超时/中断会打破整代并通知其他人。
CyclicBarrier 为什么会抛 BrokenBarrierException
屏障表达全部参与者到齐才继续。一位参与者中断、超时、reset 或 barrierAction 失败后,共同承诺已无法满足,JDK 将当前 Generation 标记 broken 并唤醒其他等待者,使其抛 BrokenBarrierException,避免部分线程错误进入下一阶段。
Semaphore 能做 QPS 限流吗
它直接限制的是同时执行数,不是单位时间请求数。吞吐还取决于每次持有许可的时间。QPS 限流通常使用令牌桶/漏桶,多实例全局限流还需要网关或分布式方案。
Phaser 适合什么场景
它适合单 JVM 中参与者动态注册/注销的多阶段任务,维护 phase、registered 和 unarrived 状态。固定参与者简单汇合优先 CyclicBarrier,只等待一批任务结束优先 CountDownLatch。
十三、错误用法与后果
| 错误做法 | 直接后果 | 深层原因 |
|---|---|---|
sleep 猜任务完成时间 | 时快浪费、时慢提前执行 | 时间不是完成事件 |
| Latch 计数过大且无超时 | 永久等待 | 没有任何线程能补齐状态变化 |
| Semaphore 无条件 release | 并发上限逐渐失效 | 许可无所有权校验 |
| Barrier parties 大于可运行线程 | 线程池饥饿 | 等待者占满线程,剩余任务无法运行 |
| barrierAction 调远程接口 | 所有参与者被拖住 | 最后到达线程在关键路径执行回调 |
| Phaser 注册后不注销 | 阶段永不推进 | unarrived 永远不归零 |
| Exchanger 不设超时 | 配对方失败后永久等 | 交换必须恰好两方汇合 |
十四、关联知识与验收
- AQS 与 JUC 工具类全过程
- BlockingQueue 全过程
- Callable、Future 与 FutureTask 全过程
- 线程池生命周期与生产排查
- JMM 与 happens-before
- JavaSE 面试知识点
完成以下任务才算掌握:
- 不看文档画出 Latch state 从 N 到 0 和共享唤醒流程;
- 写出不会错误 release 的 Semaphore 模板,并解释为什么需要 acquired 标记;
- 构造“线程池 2 个线程、Barrier 3 个参与者”的饥饿场景并定位;
- 解释 Barrier 中断为什么会让同代其他线程失败;
- 用 Phaser 动态注册并保证每条失败路径都注销;
- 面对生产线程等待,能说出要采集的状态、对应改变状态的线程和下一步证据。
