qinghuan
← 回到知识探索

后端现场 / 可下载小项目

只有一个线程等超时,为什么其他线程也一起失败?

亲手触发 CyclicBarrier 的整轮失败,再区分正常复用、reset 后的新一轮,以及汇总动作失败。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 用缺席的第三人,稳定复现整轮失败
  4. broken 表示这一轮约定失败,不是每个人都各自超时
  5. 正常完成会自动开始下一轮,失败之后要重新协调
  6. reset 后当前状态健康,旧等待者仍可能收到异常
  7. 运行完整程序:两套 JDK 各重复三次,输出经过确定性整理
  8. 先找一轮的失败发起者,再查恢复后参与的是谁
  9. CountDownLatch 和 CyclicBarrier 不是随手互换的工具
  10. 记住这三点
  11. 别把例子用过头
  12. 自己试一小步
  13. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
同一轮会合
一组参与者必须都抵达,才一起通过这一个会合点。
屏障破坏状态
这一轮的到齐约定已失败,等待者不能再按成功路径通过。
打开完整术语小抄 ↗

三个分片要会合后再汇总,其中一人等不到第三人而超时。另一位明明没有到自己的超时时间,却收到 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 后要发起新调用

正在准备实验控件…

第 1/8 步:第 1 轮需要三人
三次 await 都到达才会正常通过。这幅图把每一步固定下来,没有启动线程或计时器。

屏障当前轮次
1
当前轮是否已破坏
否
此刻等待人数
0/3
已完成的屏障动作
0

此刻正在等待:无人。下面逐次保留调用结果;开始新一轮不会把旧异常改成成功。

每一行是一轮中的一次 await 调用
轮次参与者调用结果
还没有调用 await
什么时候零时限也可能正常返回?

如果调用者恰好是最后一位到达者,人数已经齐了,可以直接完成这一轮。本轨迹中 B 是第二位,因此 await(0) 才触发超时。普通 Java 线程调度顺序不能靠这幅图推断。

三人屏障的固定轨迹图解,不启动真实线程或计时器。这里明确安排 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 一致。主线程等工作线程结束后再按固定顺序输出。

展开收起代码 · 小实验java · 137 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · java
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,导致旧新参与者混在一起。
  • 把排好序的到达编号误读成固定线程的抵达顺序。

自己试一小步

为两轮分片计算设计恢复记录:你怎样保证旧一轮的参与者不会误入新一轮?

  1. 画出三个参与者与一个协调者的两轮时间线,标出正常会合点和一个故障点。
  2. 为每轮分配业务批次编号,记录每个参与者的开始、抵达、结果和退出。
  3. 选择恢复策略并说明谁负责决定下一轮开始,旧参与者如何确认退出。
  4. 分别注入超时与汇总动作异常,保存各角色收到的异常及恢复后的状态,不根据一个异常类别推断全组结果。

做到这些,就算完成

  • 恢复动作和业务副作用的处理分开记录。
  • 同一轮有完整参与者清单,没有把旧轮次的成功算入新轮次。
  • 实验输出保持可复核,且未把本机线程屏障当成分布式协议。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • Java SE 17 CyclicBarrier:共同失败、await 与 reset (新标签页)↗Oracle Java 官方文档 · 英文

    超时破坏整轮、正常复用、到达编号、reset 及动作失败规则。

    2026-10-23 已实际搜索并打开正文,核读类说明的共同失败模型、await(long,TimeUnit)、isBroken、reset、getNumberWaiting 正文;未声称通读整页或所有实现。

  • Java SE 17 BrokenBarrierException (新标签页)↗Oracle Java 官方文档 · 英文

    等待开始前或等待过程中屏障进入破坏状态,都可能向该参与者报告这个异常。

    2026-10-23 已实际搜索并打开正文,核读异常类说明;未声称通读整页或所有实现。

  • Java SE 17 CountDownLatch:一次性计数 (新标签页)↗Oracle Java 官方文档 · 英文

    对照一次性完成通知与多轮屏障,并解释实验的同步信号。

    2026-10-23 已实际搜索并打开正文,核读类说明、计数不能重置及 Memory consistency effects;未声称通读整页或所有实现。