开始前,会这些就够了
- 认识线程和带超时的等待
- 能区分一个参与者失败与整组计算已经成功
- 先读 三个消费者只放一个结束标记,为什么程序还不退出?:先数清参与者和退出条件,再理解整组会合的失败与恢复。
三个分片要会合后再汇总,其中一人等不到第三人而超时。另一位明明没有到自己的超时时间,却收到 BrokenBarrierException。
CyclicBarrier 把同一轮会合看成共同的约定。一位参与者提前退出,这一轮就不能再假装大家都到齐;异常也会影响正在等和后来抵达的人。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。真实 CyclicBarrier、受控0纳秒超时、正常两轮、等待中reset与action失败;每 JDK 重复3次,每次18检查。其他等待上限只防挂;未测复杂中断竞争、业务回滚或性能。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
一个调用超时,整轮都会破坏;reset 后要发起新调用
正在准备实验控件…
三人屏障的固定轨迹图解,不启动真实线程或计时器。这里明确安排 A 已在等待、B 用零时限等待且 C 尚未到达;下方 Java 程序验证相应异常与重用行为。图里的轮次编号是教学标记。
用缺席的第三人,稳定复现整轮失败
实验建立需要 3 人的屏障。先让一个工作线程进入 await,并通过进入信号与 getNumberWaiting()==1 确认它已经在屏障等待;主线程成为第二人,第三人始终不来。
主线程调用 await(0, NANOSECONDS)。这次零超时是有意的故障注入:人数仍不足,就不等待而直接 TimeoutException。已经等待的工作线程收到 BrokenBarrierException。
因为这一轮已破坏,后来再调用 await 的人也得到 BrokenBarrierException。不能只重试某一个调用,就默认它会回到原来那个健康的会合点。
broken 表示这一轮约定失败,不是每个人都各自超时
发起超时的线程和旁边等待的线程,收到的异常类别可以不同。输出中的 initiator=TimeoutException 与 waiter=BrokenBarrierException 描述的是同一轮失败中的两种角色。
屏障动作只在所有参与者到齐时运行。本例用 rounds 计数,失败那一轮的值保持 0,说明没有发生一次成功汇总。
官方文档还规定中断、reset 或屏障动作失败等破坏路径。本页实跑超时、reset 和动作抛异常,不声称已经执行中断与这些故障同时发生的所有组合。
正常完成会自动开始下一轮,失败之后要重新协调
旧等待线程结束以后,主线程调用 reset,确认屏障不再 broken、等待人数归零。然后启动完整的三个参与者,让每人连续 await 两次。
两个正常轮次都得到到达编号 0、1、2,各运行一次屏障动作,最终 actions=2。为了让输出可以复核,程序在全部线程结束后排序编号;它没有声称某个固定线程每次都会得到同一个编号。
两轮之间没有调用 reset,正常会合可以自动复用。失败之后的 reset 则是显式恢复动作,必须先让旧参与者对齐状态;它不会自动重做此前分片计算,也不会回滚业务副作用。
reset 后当前状态健康,旧等待者仍可能收到异常
第二个屏障只需要两人。我们确认一人在等,然后调用 reset。旧等待者得到 BrokenBarrierException;等它退出后,查询当前屏障的 isBroken 却是 false。
这两个观察并不冲突:旧等待者所属的那一轮被打断,当前对象已经进入新的健康轮次。日志若只记录一个最后查询到的 isBroken,可能会漏掉旧一轮的失败;业务记录应带上自己的批次编号。
最后一个对照让到齐时执行的屏障动作直接抛 IllegalStateException。最后抵达、负责执行动作的主线程收到这个原始异常,其他等待者收到 BrokenBarrierException,屏障保持 broken。
运行完整程序:两套 JDK 各重复三次,输出经过确定性整理
唯一的 Main.java 使用真实线程、真实 CyclicBarrier 与 CountDownLatch。协调信号先建立必要前提,getNumberWaiting 只用作本教学实验的诊断断言,不作为建议的生产流程控制器。
除故意注入的 0 纳秒超时外,3 秒与 5 秒上限都只防止异常情况下长期挂住。没有 Thread.sleep,也没有把耗时长短当成性能结论。
2026-10-23 在 Windows、Microsoft OpenJDK 17.0.20.1 与 21.0.12 上以 --release 17 分别编译;每版运行三次,每次 18 项检查,完整 stdout 一致。主线程等工作线程结束后再按固定顺序输出。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public final class Main {
private static int checks;
private static void check(boolean ok, String message) {
checks++;
if (!ok) throw new AssertionError(message);
}
private static void awaitLatch(CountDownLatch latch) throws InterruptedException {
if (!latch.await(5, TimeUnit.SECONDS)) throw new AssertionError("Coordination timed out");
}
private static String attempt(CyclicBarrier barrier, long timeout, TimeUnit unit) {
try {
barrier.await(timeout, unit);
return "OK";
} catch (Exception failure) {
return failure.getClass().getSimpleName();
}
}
private static Thread waitingWorker(CyclicBarrier barrier, AtomicReference<String> result,
CountDownLatch entered) {
Thread worker = new Thread(() -> {
entered.countDown();
result.set(attempt(barrier, 5, TimeUnit.SECONDS));
}, "barrier-waiter");
worker.start();
return worker;
}
private static void confirmWaiting(CyclicBarrier barrier, CountDownLatch entered) throws Exception {
awaitLatch(entered);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
while (barrier.getNumberWaiting() != 1) {
if (System.nanoTime() - deadline >= 0) throw new AssertionError("Waiter did not reach barrier");
Thread.onSpinWait();
}
check(barrier.getNumberWaiting() == 1, "one worker is already waiting");
}
private static void join(Thread worker) throws InterruptedException {
worker.join(5000);
if (worker.isAlive()) {
worker.interrupt();
throw new AssertionError("Worker did not finish");
}
}
public static void main(String[] args) throws Exception {
AtomicInteger rounds = new AtomicInteger();
CyclicBarrier barrier = new CyclicBarrier(3, rounds::incrementAndGet);
AtomicReference<String> waitingResult = new AtomicReference<>();
CountDownLatch entered = new CountDownLatch(1);
Thread waiting = waitingWorker(barrier, waitingResult, entered);
confirmWaiting(barrier, entered);
// Only two of three parties can arrive; zero timeout deliberately breaks this round.
String timeoutResult = attempt(barrier, 0, TimeUnit.NANOSECONDS);
join(waiting);
check(timeoutResult.equals("TimeoutException"), "the timeout initiator gets TimeoutException");
check(waitingResult.get().equals("BrokenBarrierException"), "existing waiter sees broken round");
check(barrier.isBroken(), "barrier remains broken");
check(rounds.get() == 0, "incomplete round never runs the action");
String late = attempt(barrier, 0, TimeUnit.NANOSECONDS);
check(late.equals("BrokenBarrierException"), "late arrival sees existing broken state");
System.out.println("timeout-round: initiator=" + timeoutResult + "; waiter="
+ waitingResult.get() + "; late=" + late + "; broken=" + barrier.isBroken());
// Old participants have ended before the coordinator starts a fresh generation.
barrier.reset();
check(!barrier.isBroken() && barrier.getNumberWaiting() == 0, "reset starts a fresh generation");
CountDownLatch ready = new CountDownLatch(3);
CountDownLatch start = new CountDownLatch(1);
AtomicReference<Throwable> workerError = new AtomicReference<>();
List<List<Integer>> indexes = List.of(
Collections.synchronizedList(new ArrayList<>()),
Collections.synchronizedList(new ArrayList<>()));
List<Thread> workers = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Thread worker = new Thread(() -> {
try {
ready.countDown();
awaitLatch(start);
for (int round = 0; round < 2; round++) {
indexes.get(round).add(barrier.await(5, TimeUnit.SECONDS));
}
} catch (Throwable failure) {
workerError.compareAndSet(null, failure);
}
}, "round-worker-" + i);
workers.add(worker);
worker.start();
}
awaitLatch(ready);
start.countDown();
for (Thread worker : workers) join(worker);
if (workerError.get() != null) throw new AssertionError(workerError.get());
for (List<Integer> values : indexes) {
Collections.sort(values);
check(values.equals(List.of(0, 1, 2)), "each successful round returns all arrival indexes");
}
check(rounds.get() == 2, "successful rounds automatically reuse the barrier");
check(!barrier.isBroken(), "two successful rounds leave barrier healthy");
System.out.println("after-reset: round1=" + indexes.get(0) + "; round2=" + indexes.get(1)
+ "; actions=" + rounds.get() + "; broken=" + barrier.isBroken());
CyclicBarrier resetBarrier = new CyclicBarrier(2);
AtomicReference<String> resetResult = new AtomicReference<>();
CountDownLatch resetEntered = new CountDownLatch(1);
Thread resetWaiter = waitingWorker(resetBarrier, resetResult, resetEntered);
confirmWaiting(resetBarrier, resetEntered);
resetBarrier.reset();
join(resetWaiter);
check(resetResult.get().equals("BrokenBarrierException"), "reset rejects old waiter");
check(!resetBarrier.isBroken(), "new generation can be healthy while old waiter failed");
System.out.println("reset-waiting: old-waiter=" + resetResult.get()
+ "; current.broken=" + resetBarrier.isBroken());
CyclicBarrier failedAction = new CyclicBarrier(2, () -> {
throw new IllegalStateException("merge failed");
});
AtomicReference<String> actionWaiterResult = new AtomicReference<>();
CountDownLatch actionEntered = new CountDownLatch(1);
Thread actionWaiter = waitingWorker(failedAction, actionWaiterResult, actionEntered);
confirmWaiting(failedAction, actionEntered);
String actionOwnerResult = attempt(failedAction, 5, TimeUnit.SECONDS);
join(actionWaiter);
check(actionOwnerResult.equals("IllegalStateException"), "last arrival runs the failing action");
check(actionWaiterResult.get().equals("BrokenBarrierException"), "other waiter sees broken barrier");
check(failedAction.isBroken(), "failed action breaks the generation");
System.out.println("action-failure: runner=" + actionOwnerResult + "; waiter="
+ actionWaiterResult.get() + "; broken=" + failedAction.isBroken());
System.out.println("PASS " + checks);
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。真实 CyclicBarrier、受控0纳秒超时、正常两轮、等待中reset与action失败;每 JDK 重复3次,每次18检查。其他等待上限只防挂;未测复杂中断竞争、业务回滚或性能。
运行后对照结果
timeout-round: initiator=TimeoutException; waiter=BrokenBarrierException; late=BrokenBarrierException; broken=true
after-reset: round1=[0, 1, 2]; round2=[0, 1, 2]; actions=2; broken=false
reset-waiting: old-waiter=BrokenBarrierException; current.broken=false
action-failure: runner=IllegalStateException; waiter=BrokenBarrierException; broken=true
PASS 18
先找一轮的失败发起者,再查恢复后参与的是谁
timeout-round 同时保留超时发起者、原等待者和迟到者的结果。若只统计 TimeoutException,会漏掉受同一故障影响的其他参与者。
after-reset 的两个编号列表各为 [0,1,2],actions=2,证明完整的两个新轮次正常会合。编号已排序,不能从列表顺序推断实际线程的抵达先后。
reset-waiting 则提醒你按轮次理解日志:旧等待者失败与当前屏障健康可以同时成立。action-failure 说明即使人数到齐,汇总动作失败仍会让这一轮不能当作成功。
CountDownLatch 和 CyclicBarrier 不是随手互换的工具
CountDownLatch 的计数归零是一次性事件,不能重置计数;执行 countDown 的线程也不必在原地等待其他人。本例用它发出“已经进入”和“现在可以开始”的控制信号。
CyclicBarrier 的 await 让参与者在同一会合点等待,正常轮次可以反复使用,并带有这篇观察到的共同失败语义。需要分阶段重复会合和只等一次完成通知,是不同的协调要求。
已验证受控超时、恢复后两轮会合、等待中 reset 与动作失败。未测大规模负载、进程崩溃、分布式节点、业务回滚或复杂中断竞争。真实项目若难以协调旧参与者,官方文档建议考虑为后续工作创建新的屏障对象。
记住这三点
- 一个参与者超时可以破坏整轮,其他参与者收到的可能是
BrokenBarrierException。 - 正常会合自动复用;reset 需要额外协调,不能充当业务回滚。
- 旧轮次失败与新轮次健康可以同时出现,日志要记录自己的批次身份。
别把例子用过头
- 只捕获
TimeoutException而漏掉同一轮的其他失败。 - 看到当前
isBroken=false,就断言旧等待者不可能报错。 - 在每个 catch 中各自 reset,导致旧新参与者混在一起。
- 把排好序的到达编号误读成固定线程的抵达顺序。
自己试一小步
为两轮分片计算设计恢复记录:你怎样保证旧一轮的参与者不会误入新一轮?
- 画出三个参与者与一个协调者的两轮时间线,标出正常会合点和一个故障点。
- 为每轮分配业务批次编号,记录每个参与者的开始、抵达、结果和退出。
- 选择恢复策略并说明谁负责决定下一轮开始,旧参与者如何确认退出。
- 分别注入超时与汇总动作异常,保存各角色收到的异常及恢复后的状态,不根据一个异常类别推断全组结果。
做到这些,就算完成
- 恢复动作和业务副作用的处理分开记录。
- 同一轮有完整参与者清单,没有把旧轮次的成功算入新轮次。
- 实验输出保持可复核,且未把本机线程屏障当成分布式协议。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java SE 17 CyclicBarrier:共同失败、await 与 reset (新标签页)↗
超时破坏整轮、正常复用、到达编号、reset 及动作失败规则。
- Java SE 17 BrokenBarrierException (新标签页)↗
等待开始前或等待过程中屏障进入破坏状态,都可能向该参与者报告这个异常。
- Java SE 17 CountDownLatch:一次性计数 (新标签页)↗
对照一次性完成通知与多轮屏障,并解释实验的同步信号。