Skip to content

JUC 并发协作工具全过程

java.util.concurrent 不只是线程池。它把“等待一组任务结束、限制并发、多个线程阶段汇合、动态注册参与者、两个线程交换数据”等协作协议封装成可验证的工具。本章以 JDK 7/8 为主,既讲 API,也讲状态如何变化、线程为何阻塞、谁负责唤醒、失败如何传播,以及错误使用为何会永久卡住生产线程。

学习目标

  • 根据业务语义选择 CountDownLatchSemaphoreCyclicBarrierPhaserExchanger,而不是背 API;
  • 理解 AQS 共享模式如何支撑 Latch 与 Semaphore;
  • 理解 CyclicBarrier 的 countGeneration、Condition 和 broken 状态;
  • 正确处理超时、中断、许可泄漏、屏障损坏和动态参与者注销;
  • 能构造故障并从 jstack、指标和业务日志中定位等待原因;
  • 区分线程协作工具、线程池并发度、连接池容量和分布式限流。

一、先按“谁等谁”选择

mermaid
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 个线程互相等待
Phaserphase、注册数、未到达数动态参与者注册/到达/注销多阶段动态多阶段工作流
Exchanger两个线程的交换槽两个参与者 exchange可复用双方在汇合点交换对象

它们都是单 JVM 内线程协作工具,不会跨应用实例同步。分布式部署中,每个实例各有自己的 Latch/Semaphore,不能拿它们实现全局限流或分布式屏障。

二、阻塞与唤醒的共同基础

线程等待不是靠不停轮询:

mermaid
flowchart TD
    A["线程检查条件"] --> B{"条件满足吗"}
    B -- "是" --> C["继续执行"]
    B -- "否" --> D["进入等待队列"]
    D --> E["LockSupport.park挂起"]
    F["其他线程改变状态"] --> G["选择等待节点"]
    G --> H["LockSupport.unpark唤醒"]
    H --> A

park/unpark 是底层阻塞原语之一;Latch、Semaphore 通常通过 AQS 的共享队列实现,CyclicBarrier 使用 ReentrantLock + Condition。调用者不应依赖内部队列的具体节点布局,但要理解:等待必须有状态条件,状态变化后必须有唤醒,醒来后仍要重新检查条件。

更完整的 AQS 队列、CAS、共享传播原理见 AQS 与 JUC 工具类全过程

三、CountDownLatch:等待 N 个事件

3.1 不是“等待 N 个线程”

构造参数存入 AQS 的 state,它表示 N 个事件,不强制对应 N 个线程。同一线程可完成多个事件,也可由线程池复用线程完成。

java
CountDownLatch latch = new CountDownLatch(3);
  • await():当 state > 0,当前线程进入 AQS 共享等待;
  • countDown():CAS 把 state 减一,但不会降到负数;
  • 从 1 减到 0 的线程触发共享释放,传播唤醒所有等待者;
  • state == 0 后调用 await() 立即返回;
  • 归零后不能恢复,额外 countDown() 没有效果。

3.2 状态与唤醒流程

mermaid
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 --> D

3.3 JDK 7 可运行 Demo:服务启动门闩

java
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 正确获取与释放模板

java
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 获取流程

mermaid
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 公平与非公平

java
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:保护下游

java
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

mermaid
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 一个线程失败为什么影响所有人

mermaid
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:两阶段对账

java
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 深度比较

维度CountDownLatchCyclicBarrier
关系等待者等完成事件参与者互相等待
状态改变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

java
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),双方分别得到对方对象。它交换的是对象引用,不会复制对象,之后双方同时修改该可变对象仍可能数据竞争。

mermaid
flowchart TD
    A["线程A携带对象A到交换点"] --> C["等待配对"]
    B["线程B携带对象B到交换点"] --> C
    C --> D["线程A得到对象B"]
    C --> E["线程B得到对象A"]
java
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

mermaid
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 通用证据

  1. 固定故障时间、接口、批次号和线程池名;
  2. 连续获取至少两到三份线程栈,判断是瞬时等待还是长期不变;
  3. 查看线程池 active、queue、completed、reject 和最老任务年龄;
  4. 记录工具关键状态:Latch count、Semaphore available/queue length、Barrier waiting/broken、Phaser phase/registered/unarrived;
  5. 找“本应改变状态”的线程为什么没运行、失败或卡在 I/O;
  6. 先用超时、限流、降级止损,再修复计数、生命周期或线程池拓扑。

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 7JDK 8现代 JDK
CountDownLatch/Semaphore/CyclicBarrier/Exchanger可用可用语义持续兼容
PhaserJDK 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 不设超时配对方失败后永久等交换必须恰好两方汇合

十四、关联知识与验收

完成以下任务才算掌握:

  • 不看文档画出 Latch state 从 N 到 0 和共享唤醒流程;
  • 写出不会错误 release 的 Semaphore 模板,并解释为什么需要 acquired 标记;
  • 构造“线程池 2 个线程、Barrier 3 个参与者”的饥饿场景并定位;
  • 解释 Barrier 中断为什么会让同代其他线程失败;
  • 用 Phaser 动态注册并保证每条失败路径都注销;
  • 面对生产线程等待,能说出要采集的状态、对应改变状态的线程和下一步证据。