承接的问题
这三个同步器回答同一个问题:一组线程如何在一个点上会合。差别在生命周期——CountDownLatch 一次性,CyclicBarrier 按“代”重用,Phaser 按阶段推进且参与者可以动态增减。
4.1 的信号量数的是“资源”,可以被任何线程 release;这三者数的是“事件”,参与者身份是显式的,谁到达、到达几次都有明确语义。选错的典型症状是:用闩锁做周期性任务,第二轮直接空转;用栅栏做数量可变的会合,构造时就把 parties 写死。
CountDownLatch:一次性、共享模式
CountDownLatch 的实现是三者里最短的,全部逻辑就是一个 AQS 子类:
// JDK 25 源码摘录,已删减
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
await() 走 acquireSharedInterruptibly(1),countDown() 走 releaseShared(1),所以等待者在共享队列上排队,和 1.3 讲的传播链完全一致。两个细节值得对照 4.1 看:
tryReleaseShared只有把计数减到 0 的那一次返回true,只有那一次触发唤醒;Semaphore.tryReleaseShared则每次返回true,因为每个许可都对应一个可放行的等待者。- 计数只减不增,也没有复位入口。
countDown()在计数已经是 0 时返回false并且什么都不做——它不报错,只是静默失败。这正是“不可重用”的实现层面含义:第二轮调用countDown()不会有任何效果,第二轮await()会立刻返回。
getCount() 就是读 state,可以在监控里用,但不要拿它做“还差几个”的业务判断,读到的值随时会变。
CyclicBarrier:ReentrantLock + Condition,不是 AQS 子类
CyclicBarrier 没有继承 AQS。它用一把 ReentrantLock 加一个条件队列,加一个“代”对象:
// JDK 25 源码摘录,已删减
private static class Generation {
Generation() {} // prevent access constructor creation
boolean broken; // initially false
}
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/** The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();
private int count;
为什么不用 AQS?因为栅栏要维护的是“一整代”的状态:剩余人数、是否被打破、以及“换代”这个整体动作。用锁加条件队列表达更直接:dowait 在锁内执行 --count,减到 0 的线程执行 barrierCommand,然后 nextGeneration() 做三件事——trip.signalAll() 唤醒所有人、count = parties 复位、换一个新的 Generation 对象。
等待者在 for (;;) 循环里调用 trip.await(),醒来后要重新检查三件事:这一代是否被打破、generation 是否已经换人、以及是否超时。对条件队列的循环等待与虚假唤醒,卷一第 5 章已经讲过,这里是同一个模式。
这个设计带来两个重要语义:
全员或全无。 任何一个等待者被中断或超时,都会调用 breakBarrier():把 generation.broken 置为 true、count 复位、signalAll()。其他等待者醒来看到 broken 就抛 BrokenBarrierException,已经通过栅栏的线程不受影响。业务上要么整批一起走,要么整批失败重来,不存在“走了一半”。
broken 不会自愈。 被打破的这一代已经作废,后续到达的线程看到 g.broken 会立刻抛异常。要恢复必须显式调用 reset(),它会 breakBarrier() 再 nextGeneration(),等于人为作废当前代。这个语义经常被忽略:一次超时之后如果不 reset,这个栅栏对后来者就是永久抛异常的。
还有一点:barrierCommand 是在持锁状态下执行的。命令里做慢操作会阻塞所有后来者,命令里再调用 barrier.await() 会直接卡死。
Phaser:一个 long 拆三段
Phaser 既不是 AQS 子类,也不用锁,它把所有状态压进一个 volatile long,用 VarHandle 做 CAS:
// JDK 25 源码摘录,已删减
private volatile long state;
private static final int MAX_PARTIES = 0xffff;
private static final int MAX_PHASE = Integer.MAX_VALUE;
private static final int PARTIES_SHIFT = 16;
private static final int PHASE_SHIFT = 32;
private static final int UNARRIVED_MASK = 0xffff; // to mask ints
private static final long PARTIES_MASK = 0xffff0000L; // to mask longs
private static final long COUNTS_MASK = 0xffffffffL;
private static final long TERMINATION_BIT = 1L << 63;
private static final int ONE_ARRIVAL = 1;
private static final int ONE_PARTY = 1 << PARTIES_SHIFT;
private static final int ONE_DEREGISTER = ONE_ARRIVAL|ONE_PARTY;
private static final int EMPTY = 1;
private static int unarrivedOf(long s) {
int counts = (int)s;
return (counts == EMPTY) ? 0 : (counts & UNARRIVED_MASK);
}
private static int partiesOf(long s) {
return (int)s >>> PARTIES_SHIFT;
}
private static int phaseOf(long s) {
return (int)(s >>> PHASE_SHIFT);
}
位布局可以画成一张表:
| 位 | 宽度 | 含义 |
|---|---|---|
| 63 | 1 | TERMINATION_BIT,终止标记 |
| 32–62 | 31 | phase,阶段号,到 Integer.MAX_VALUE 后回绕 |
| 16–31 | 16 | parties,已注册的参与者数 |
| 0–15 | 16 | unarrived,还没到达的人数 |
压进一个 long 的理由和 3.1 的读写锁把读写计数压进 64 位一样:一次 CAS 同时更新三个字段。如果 phase、parties、unarrived 分成三个字段,到达操作就要维护三者的一致性,任何一步失败都可能留下“人数减了但阶段没推进”的中间态。
doArrive 的主路径也因此很短:CAS 把 unarrived 减一;如果减之前 unarrived 是 1(自己是最后一个),就调用 onAdvance(phase, nextUnarrived),返回值决定是置终止位还是推进到 phase + 1,然后 releaseWaiters(phase) 唤醒等待这一阶段的线程。等待者不在 AQS 队列里,而是压在两个 QNode Treiber 栈上——evenQ 和 oddQ 按阶段奇偶分栈,注释说这是为了“释放一部分线程的同时加入另一部分”时减少竞争。
和 CyclicBarrier 的关键差异:
- 参与者可变。 构造时给初始 parties,之后可以
register()增加、arriveAndDeregister()减少;CyclicBarrier的 parties 是 final。 - 阶段是数字。
getPhase()返回递增的阶段号,没有“代对象”;onAdvance返回true表示终止,之后arrive返回负数,register也不会恢复。 - 没有内置的“最后一人动作”参数。 要做和
barrierCommand一样的事,只能覆盖onAdvance。 - 可以分层。 构造时传入 parent,子 Phaser 的到达会向父级传播,适合把大团队拆成小组。
arriveAndAwaitAdvance() 的 javadoc 写的是“返回到达阶段的编号”,但 JDK 25 的实现返回的是推进后的 phase:本机输出里第一轮 returned 1、getPhase() 也是 1。写代码时不要依赖“返回值等于进入时的 getPhase()”这个假设。
同一场景的三种写法
libraryd-conc 每晚要把 6 个分片目录加载进内存,全部就绪后才发布快照。下面这份程序用三种同步器各写一遍,每个分片的加载耗时固定为 20 × (id + 1) 毫秒,便于对照:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Phaser;
import java.util.concurrent.atomic.AtomicLong;
public final class ThreeWaysCompact {
static final int N = 6;
public static void main(String[] args) throws Exception {
latch();
barrier();
phaser();
}
static long ms(long t0) { return (System.nanoTime() - t0) / 1_000_000; }
static void load(int id) {
try { Thread.sleep(20L * (id + 1)); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
static void latch() throws Exception {
CountDownLatch latch = new CountDownLatch(N);
long t0 = System.nanoTime();
for (int i = 0; i < N; i++) {
int id = i;
Thread.ofPlatform().start(() -> { load(id); latch.countDown(); });
}
latch.await();
System.out.printf("[latch] round1 publish at %d ms, count=%d%n", ms(t0), latch.getCount());
long t1 = System.nanoTime();
for (int i = 0; i < N; i++) {
Thread.ofPlatform().start(() -> {
try { Thread.sleep(100); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
latch.countDown(); // count 已经是 0,这次调用没有效果
});
}
latch.await();
System.out.printf("[latch] round2 publish at %d ms (workers still loading)%n", ms(t1));
}
static void barrier() throws Exception {
AtomicLong roundStart = new AtomicLong(System.nanoTime());
CyclicBarrier barrier = new CyclicBarrier(N + 1, () ->
System.out.printf("[barrier] publish at %d ms%n", ms(roundStart.get())));
for (int i = 0; i < N; i++) {
int id = i;
Thread.ofPlatform().start(() -> {
try {
for (int round = 0; round < 2; round++) { load(id); barrier.await(); }
} catch (Exception e) { throw new RuntimeException(e); }
});
}
for (int round = 0; round < 2; round++) {
roundStart.set(System.nanoTime());
barrier.await();
}
System.out.printf("[barrier] two generations done, waiting=%d%n", barrier.getNumberWaiting());
}
static void phaser() throws Exception {
Phaser phaser = new Phaser(N + 1) {
@Override protected boolean onAdvance(int phase, int registered) {
System.out.printf("[phaser] onAdvance phase=%d registered=%d%n", phase, registered);
return registered == 0;
}
};
for (int i = 0; i < N; i++) {
int id = i;
Thread.ofPlatform().start(() -> {
for (int round = 0; round < 2; round++) { load(id); phaser.arriveAndAwaitAdvance(); }
});
}
for (int round = 0; round < 2; round++) {
int returned = phaser.arriveAndAwaitAdvance();
System.out.printf("[phaser] publish at phase=%d (returned %d), registered=%d%n",
phaser.getPhase(), returned, phaser.getRegisteredParties());
}
}
}
本机真实输出(javac --release 25 编译,单次运行,耗时会随调度波动):
[latch] round1 publish at 130 ms, count=0
[latch] round2 publish at 3 ms (workers still loading)
[barrier] publish at 123 ms
[barrier] publish at 123 ms
[barrier] two generations done, waiting=0
[phaser] onAdvance phase=0 registered=7
[phaser] publish at phase=1 (returned 1), registered=7
[phaser] onAdvance phase=1 registered=7
[phaser] publish at phase=2 (returned 2), registered=7
三段代码的等待语义完全一致:慢分片 id=5 睡 120 ms,几次发布都在 120–135 ms 之间,说明发布确实发生在最后一个分片加载完之后。差异在第二轮:
- 闩锁第二轮在 3 ms 就“发布”了,因为
count已经是 0,await()立即返回,而新一批工作线程还在睡 100 ms。这就是一次性的代价。 - 栅栏第二轮在 123 ms 发布,因为
nextGeneration()把count复位成 7,signalAll之后所有人重新排队。 - Phaser 第二轮在
phase=2发布,onAdvance在第一轮结束和终止时各被调用一次。
在 libraryd-conc 里落地
分片数不固定时,栅栏和闩锁都不合适:CyclicBarrier 的 parties 在构造时写死,CountDownLatch 的计数一旦设定只能减少。Phaser 的 register / arriveAndDeregister 正好覆盖两种情况——启动时按实际分片数注册,某个分片没有数据时直接注销:
package com.plume.conc.core;
import java.util.List;
import java.util.concurrent.Phaser;
/** 每轮按实际分片数注册,加载完再注销;空分片通过 arriveAndDeregister 退出。 */
public final class SnapshotPipeline {
private final Phaser phaser = new Phaser(1); // 1 是协调线程自己
public void loadRound(List<ShardLoader> shards) throws InterruptedException {
for (ShardLoader shard : shards) {
phaser.register();
Thread.ofPlatform().name("shard-" + shard.id()).start(() -> {
try {
if (shard.isEmpty()) {
phaser.arriveAndDeregister(); // 不参与本轮会合
} else {
shard.load();
phaser.arriveAndAwaitAdvance();
}
} catch (RuntimeException e) {
phaser.arriveAndDeregister();
throw e;
}
});
}
phaser.arriveAndAwaitAdvance(); // 协调线程在这里等齐
}
public int phase() {
return phaser.getPhase();
}
public interface ShardLoader {
int id();
boolean isEmpty();
void load();
}
}
两个边界要留意。第一,注册必须发生在启动线程之前,否则线程可能先 arrive、协调线程后 register,人数永远凑不齐;第二,异常路径也要 arriveAndDeregister(),否则这一轮永远等不到最后一个人,表现为协调线程永久挂起。这也是 Phaser 比闩锁难用的地方:每个参与者都必须保证“要么到达,要么注销”。
日常的停机协调仍然更适合 CountDownLatch:它只减不加、没有复位,语义和“等所有组件关闭”完全吻合,也不需要担心参与者注销。
这个设计的代价
闩锁不可重用,这是设计而不是缺陷。 它换来的是实现最短、语义最窄:只有一个计数,只有归零这一个事件。周期性任务请直接用栅栏或 Phaser。
栅栏的唤醒是 signalAll。 每一代结束都会唤醒所有等待者,然后他们还要重新竞争锁才能返回;参与人数多时这是一次惊群。Phaser 按阶段只唤醒等待该阶段的节点,粒度更细。
栅栏的命令在锁内执行。 barrierCommand 慢,整代参与者都慢;命令抛异常会直接打破栅栏。
Phaser 的状态机最难调试。 phase 回绕、终止位、父级传播、注销与到达的交错,任何一个用错都表现为“人数对不上”。它适合参与者结构会变的场景,不适合拿来替代简单的闩锁。
什么时候不要用
参与者在同步点上做长耗时的工作,三个同步器都不合适。同步点只应该承担“会合”,耗时的加载、计算要在到达之前完成,否则一个慢参与者会拖住所有人,而且从线程栈上只能看到 park,看不出是谁慢——这类问题卷三第 6 章展开。
需要“部分成功也能继续”的场景不要用栅栏。CyclicBarrier 的语义是全有或全无,一个超时就让整代失败;如果业务允许部分分片失败,应该用 Phaser 加自己的成功计数,或者根本不用会合点,改用 CompletableFuture.allOf 这类带结果聚合的机制(第 13 章)。
最后,不要用 CountDownLatch 做“等待某个条件成立”。它只能等计数归零,不能表达“库存大于 0”这种可反复变化的条件;那类需求用条件队列或 Semaphore。
小结
CountDownLatch 把计数放在 AQS 的 state 里,只有归零那一次释放会唤醒等待者,且永不复位;CyclicBarrier 用 ReentrantLock 加 Condition 维护“代”,signalAll 实现整代放行,broken 一旦置位必须 reset() 才能恢复;Phaser 把 phase、parties、unarrived 压进一个 long,一次 CAS 完成到达与推进,支持动态参与者与分层。三者的选择标准不是性能,而是生命周期:一次、按代、按阶段。
阅读导航:上一节 4.1 Semaphore 与许可模型 | 下一节 4.3 Exchanger 与同步器组合
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。