线程状态、中断、取消与超时
一次异步计算同时涉及任务、执行线程和结果句柄。调用者可以停止等待结果,任务也可以被标记为取消;执行线程何时停止、文件和连接何时释放,还取决于任务代码及其阻塞操作。
调用者
├─ 提交任务 Runnable / Callable
│ └─ 线程执行任务体 → 使用资源 → 归还资源 → 退出任务
└─ 持有 Future
├─ get:等待或取得结果
└─ cancel:尝试使结果进入取消状态线程是执行载体,Runnable 描述没有返回值的工作,Callable 可以返回结果并抛出异常。Future 保存计算结果的状态;同一个线程池线程可能连续执行多个任务,因此“任务结束”和“线程终止”也需要分别观察。
启动线程与等待结束
run、start 与 join
下面的完整程序在主线程中构造一个平台线程,将计算交给它,再等待它结束:
public final class ThreadStart {
public static void main(String[] args) throws InterruptedException {
int[] result = new int[1];
Thread worker = new Thread(() -> result[0] = 6 * 7, "sum-worker");
System.out.println("before=" + worker.getState());
worker.start();
worker.join();
System.out.println("result=" + result[0]);
System.out.println("after=" + worker.getState());
}
}在 Linux Bash 中以普通用户保存为 ThreadStart.java,使用完整 JDK。示例以 Temurin 25.0.4+7 为运行环境;这些基础 API 同样可在 JDK 17 执行。JDK 安装和编译目标的区别见Java 平台、JDK 与工程版本基线。
export JAVA_HOME=/opt/jdk-25
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
"$JAVA_HOME/bin/java" -version
"$JAVA_HOME/bin/javac" -version
LAB_OUT=$(mktemp -d /tmp/thread-learning.XXXXXX)
"$JAVA_HOME/bin/javac" --release 17 -Xlint:all -Werror -d "$LAB_OUT" ThreadStart.java
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" ThreadStart预期输出:
before=NEW
result=42
after=TERMINATEDstart 安排新线程执行;直接调用平台 Thread 对象的 run 只是当前线程上的方法调用,不会创建并行执行。每个 Thread 对象最多启动一次,结束后再次 start 会抛 IllegalThreadStateException。创建下一个任务时,应新建线程或提交给仍开放的执行器。Thread API规定了这些生命周期行为;虚拟 Thread 的 run 不用于直接启动任务。
这里使用无参 join,是因为线程只做有限次整数运算。它等待的是目标线程终止,目标中的动作 happens-before 等待者检测到终止后的动作,因此主线程在 join 返回后能读到 42;不能把 join 删除,再用 sleep 猜测对方是否计算完。跨线程可见性的完整规则见JLS 17.4。
真实任务可能阻塞,应使用有界 join,再检查 isAlive。join(2000) 返回可能因为线程结束,也可能因为等待时限已到;join(0) 则表示无限等待,不能把已经耗尽的预算转成 0 后继续传入。
启动示例结束后可删除这次编译目录,源码保留;后面的取消实验会创建自己的目录:
case "$LAB_OUT" in
/tmp/thread-learning.*) rm -r -- "$LAB_OUT" ;;
*) printf '保留未知目录\n' >&2 ;;
esac
unset LAB_OUT名称、异常与 daemon
线程名称有助于在日志和转储中找到执行者,但不是唯一 ID,也不自动携带请求上下文。Runnable 抛出的未捕获异常会结束该线程,并进入 UncaughtExceptionHandler;通过 submit 执行的任务,其异常通常由 Future 保存,需要调用者读取和处理。
daemon 属性决定该线程是否阻止 JVM 开始关闭,不保证任务清理。平台线程可在启动前设置 daemon;虚拟线程始终为 daemon。重要的写入或刷盘不能依赖 JVM 等待后台线程“自然收尾”,应由任务所有者显式等待。虚拟线程的成本和调度方式见上下文、CompletableFuture、ForkJoin 与虚拟线程。
线程状态与阻塞行为
Java 的六种状态描述 JVM 可观察的执行状态,不能直接替代操作系统调度状态或业务任务状态。Thread.State给出了状态定义。
| 状态 | 常见原因 | 需要确认的对象 |
|---|---|---|
| NEW | 已构造,尚未启动 | 谁负责调用 start |
| RUNNABLE | 正在执行或等待某些系统资源 | CPU 样本、具体栈帧、系统调用 |
| BLOCKED | 等待进入或重新进入对象 monitor | 正在竞争哪个锁、谁持有它 |
| WAITING | 无期限 wait、join 或 park | 条件、被等待线程或唤醒协议 |
| TIMED_WAITING | sleep 或带时限的等待 | 剩余预算、等待原因 |
| TERMINATED | run 正常返回或异常结束 | 结果、异常与清理是否完成 |
空闲线程池 worker 等待队列是正常状态;RUNNABLE 也可能包含某些阻塞 I/O。getState 适合诊断,不适合用作业务同步条件:读取之后状态可以立刻变化。
wait 被通知后仍要重新取得锁
wait 必须在拥有目标 monitor 时调用。它把线程加入等待集合并释放这个 monitor;其他已经持有的锁不会一并释放。notify/notifyAll 使等待者有机会继续,但通知者仍然持有锁,等待者必须重新取得同一 monitor,wait 才能返回。
持有 monitor
│ wait:释放该 monitor
▼
WAITING / TIMED_WAITING
│ 通知、到期、中断或伪唤醒
▼
重新竞争 monitor
├─ 暂时取不到锁 → BLOCKED
└─ 取得锁 → wait 返回或抛 InterruptedException
↓
再检查条件重新竞争可能非常短,线程快照未必抓到 BLOCKED;图中的关键是取得锁这个条件,而非保证每次都观察到所有状态。即使因中断退出 wait,也要先恢复 monitor 的持有,再抛出 InterruptedException。通知与中断竞争时还有规范允许的不同处理次序,不能把一次观察的顺序当成唯一结果。详见Object.wait和JLS 等待与通知。
等待应放在检查条件的循环中:
synchronized (monitor) {
while (!ready) {
monitor.wait();
}
consumeReadyData();
}ready 的读取和修改使用同一个 monitor。循环同时处理伪唤醒、其他消费者先取走数据和通知不代表条件成立的情况。wait/notify 的队列关系与字节码由Monitor 与 synchronized继续展开。
sleep、park 和锁等待的区别
sleep 不释放持有的 monitor,所以不要在长临界区里睡眠等待另一个线程完成同锁工作。LockSupport.park 使用线程许可,许可最多保留一个;unpark 可以先于 park 发生,多次 unpark 不累积多个令牌。park 还可能因为中断或伪唤醒返回,所以返回后仍须检查业务条件。它不会像 sleep 那样抛 InterruptedException 或清除中断标志。LockSupport API明确列出了返回条件。
| 阻塞位置 | 中断后的典型行为 | 编写任务时的处理 |
|---|---|---|
| sleep、wait、join | 抛 InterruptedException,清除中断标志;wait 先重获 monitor | 传播异常或终止当前任务 |
| JUC 的可中断等待 | 按对应 API 抛 InterruptedException | 不吞掉取消,处理已取得的资源 |
| synchronized 进入竞争 | 不因中断直接放弃 monitor 竞争 | 需要可取消获取时选 lockInterruptibly 等 API |
| park | 返回,保留中断标志 | 检查条件和中断,防止立即 park/返回忙转 |
| InterruptibleChannel 的阻塞操作 | 关闭通道,抛 ClosedByInterruptException,保留中断标志 | 按通道生命周期处理,不能继续当原连接可用 |
| 未承诺可中断的 I/O 或 native 调用 | 取决于具体实现 | 设置操作自身超时,按 API 设计关闭或取消 |
InterruptibleChannel的关闭语义不能推广到所有传统 InputStream 或第三方驱动。线程层发出请求之后,实际能中止到哪一层,要逐个检查阻塞 API。
中断标志怎样传到任务
interrupt 是发送给线程的协作信号。普通计算不会因为标志改变就突然退出;任务需要在可安全停止的位置检查状态,可中断阻塞方法则会把信号转成异常或提前返回。
isInterrupted 读取指定线程的标志。静态方法 Thread.interrupted 读取并清除当前线程的标志,哪怕通过某个变量调用静态方法,检查的仍是当前线程。重复发送 interrupt 不形成可计数消息;需要处理每一项命令时应使用队列,而不是把中断当作事件总线。
用真实输出区分读取、清除和返回
下载完整实验包,解压后进入 threads-interrupt-cancel。包内的 CancellationLab.java不依赖第三方库,使用 Java 17 可用 API。继续使用前面的 JAVA_HOME;新开 Shell 时先配置完整 JDK 并清除额外 JVM 环境参数。
test -x "$JAVA_HOME/bin/javac"
LAB_OUT=$(mktemp -d /tmp/cancellation-learning.XXXXXX)
"$JAVA_HOME/bin/javac" --release 17 -Xlint:all -Werror \
-d "$LAB_OUT" CancellationLab.java
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab flagsread=true consumed=true after-clear=false
sleep=InterruptedException flag=false
park=returned flag=true第一行先设置标志,再用 isInterrupted 读取,最后用 Thread.interrupted 消费;第二行进入 sleep 时已被中断,sleep 抛异常并清除标志;第三行则在同样的已中断状态下调用 park,方法返回而标志仍为 true。三个结果来自不同 API 的契约。
传播、退出与恢复各自适用的位置
底层可中断方法如果声明 throws InterruptedException,通常直接传播,使调用者决定整个任务的终止:
static void waitForWork(BlockingQueue<Job> queue) throws InterruptedException {
Job job = queue.take();
job.execute();
}Runnable.run 不能声明这个受检异常,任务最外层可以捕获后结束,并在 finally 中释放自己取得的资源:
try {
while (!Thread.currentThread().isInterrupted()) {
Job job = queue.take();
process(job);
}
} catch (InterruptedException cancelled) {
Thread.currentThread().interrupt();
} finally {
closeOwnedResources();
}以上是接入骨架,Job、queue 和资源由应用提供。关键是 catch 后不再回到取任务循环。恢复标志让上层清理逻辑仍能看到取消,但“恢复后继续同一阻塞循环”可能更糟:下一次阻塞立即因标志抛错,又被捕获并恢复,最终变成高 CPU 忙转。
长计算需要设置检查点,例如每处理一批记录检查一次。取消延迟包含等到检查点、结束当前操作和完成清理的时间;检查越密不一定越好,应该把检查放在不会破坏当前数据结构或外部写入协议的位置。不能用 Thread.stop/suspend/resume 修补不合作的任务:这些历史 API 不提供安全的资源与不变量恢复方式,现代 JDK 的行为还可能直接拒绝调用。
稳定观察吞掉中断的后果
合作模式和反例使用相同的门闩,先确认任务体已开始,再请求取消:
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab cooperative
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab ignored合作模式输出:
cancelled=true alive=false cleaned=true
after-release: alive=false cleaned=true反例在捕获中断后故意继续等待另一个释放信号,输出:
cancelled=true alive=true cleaned=false interrupt-observed=true
after-release: alive=false cleaned=true反例的第一行确认中断已经到达,Future 已取消,但任务继续运行且未执行清理。第二行来自实验控制器随后发出的独立释放信号,确保负例也能结束;它不是 cancel 的效果。线程等待使用有界握手,不靠固定 sleep 猜测竞争位置,也不会故意在后台留下无限循环。
FutureTask 的结果竞争与执行退出
Future是结果接口。get 可以正常返回、抛 ExecutionException、CancellationException、InterruptedException,带时限的形式还可能抛 TimeoutException。最后一种只终止此次等待,不主动取消计算。
开始前、运行中与完成后的取消
实验明确使用 FutureTask:它同时实现 Runnable 和 Future,可以直接交给一个可命名的线程。先比较开始前和完成后两个没有时序歧义的窗口:
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab before
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab completedaccepted=true cancelled=true done=true body-ran=false
result=42 cancel-accepted=false cancelled=false第一种在 run 之前成功取消,后来即使调用 run,任务体也不执行。第二种已提交正常结果,后来的 cancel 失败,42 仍然可以读取。多个线程竞争完成或取消时,不能只观察“有人发起过 cancel”。
再比较运行中的 cancel(false) 和 get 超时:
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab cancel-false
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab timeoutcancelled=true alive=true cleaned=false interrupt-observed=false
after-release: alive=false cleaned=true
get=TimeoutException done=false alive=true
after-release: alive=false cleaned=true对于这里的 FutureTask,cancel(false) 可以在运行中赢得结果状态,但不请求中断 runner;任务继续执行,计算返回的值也不会覆盖取消结果。get 超时则连结果状态也没改变,释放任务后仍能正常取得 42。不同 Future 实现的取消机制可能不同,接口返回值也不应当成所有竞争情形下的唯一查询,应结合 isCancelled 和具体实现的取消约定。
state、runner 与完成通知
OpenJDK jdk-25+36 的 FutureTask把结果状态与执行者分别保存在 state、runner 等字段中。任务体正在运行时,state 仍可能是 NEW;NEW 在这里不是 Thread.State.NEW。
FutureTask.state
NEW
├─ 完成方赢得更新 → COMPLETING → NORMAL / EXCEPTIONAL
├─ cancel(false) 赢得更新 → CANCELLED
└─ cancel(true) 赢得更新 → INTERRUPTING → INTERRUPTED
Thread
NEW → 执行 FutureTask.run / Callable.call → 任务清理 → 线程结束
runner 记录执行线程完成方先竞争 NEW→COMPLETING,再保存 outcome 并发布终态。取消方也必须先赢得从 NEW 出发的原子更新;成功取消才继续后续动作。cancel(true) 路径进入 INTERRUPTING,读取 runner 并尝试 interrupt,最后发布 INTERRUPTED,唤醒等待结果的线程。
run 结束时清空 runner,并与可能尚在发送中断的取消方协调。这样,取消发送动作不会无约束地拖到执行器让该 worker 承接下一项任务之后。这个握手并不会代替任务体处理取消,也不保证被调用的驱动立即退出。
在这个固定版本中,FutureTask.isDone 检查 state != NEW,连短暂的 COMPLETING 和 INTERRUPTING 都会返回 true。get 遇到 COMPLETING 仍需等待结果发布,遇到取消状态则报告 CancellationException。isDone 尤其不能用来判断 Callable 的 finally 是否已经执行完。等待者需要知道资源是否归还时,应由任务发布独立的完成信号,或通过执行器终止、实际线程 join 等与所管理对象相匹配的方式确认。在线程池中不能 join 某个长期 worker 来代表单个任务完成。
CompletableFuture.cancel不使用 mayInterruptIfRunning 控制底层处理。把 FutureTask 的 runner 机制套到 CompletableFuture,会误以为取消完成图就能终止供应结果的 I/O。
截止时间与剩余预算
超时时长是一次操作最多等待多久,截止时间则约束整个任务还剩多久。800 ms 的入口预算中,排队花掉 700 ms,后续操作只能使用剩余约 100 ms;给每一层重新分配 800 ms,会让排队、重试和下游等待相加。
在一个 JVM 内使用单调时间
System.nanoTime适合计算经过的时间,其起点没有跨进程意义。不要把它的数值发给另一个 JVM 当公共时钟;跨服务可传剩余预算,或采用已经定义时钟与传输误差的协议。
实验中的 Deadline 保存起点和预算,剩余量采用减法计算:
long elapsed = clock.getAsLong() - start;
long remaining = budget - elapsed;
if (remaining <= 0) {
throw new TimeoutException("deadline exceeded");
}
return remaining;完整类在 CancellationLab.java 中,默认 clock 为 System::nanoTime,并把单次示例预算限制在 (0, 1 天],避免不合理的超长区间。测试使用可控单调时钟推进预算,不涉及真实网络请求:
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab deadlinebudget=800ms queue=700ms remaining=100ms
remaining=0 action=TimeoutException接入 Future 时保留纳秒精度:
return future.get(deadline.remainingNanos(), TimeUnit.NANOSECONDS);每次排队、重试或下游调用前重新计算。只能接收毫秒的驱动还要确认 0 的语义;若不足一个最小单位,不应无条件截断成 0 导致无限等待,也不能假装向上取整仍严格满足原预算。
超时以后如何处理工作
调用者独占的 Future 可以在 TimeoutException 后请求取消,但取消仍需合作。调用者本身被中断时,也应按任务所有权决定是否取消子任务,并继续抛出 InterruptedException 或在最外层恢复标志。
try {
return future.get(deadline.remainingNanos(), TimeUnit.NANOSECONDS);
} catch (TimeoutException | InterruptedException failure) {
future.cancel(true); // 仅适用于当前调用者拥有的任务
throw failure;
}共享缓存刷新或多个请求共用的计算,不能因某一个等待者超时就被它擅自取消。结果无人等待和工作仍有价值是两个不同条件。
线程停止也无法撤销已经提交的 SQL 或已经到达对端的 HTTP 请求。写操作超时后,要按幂等键、状态查询或补偿协议判断业务结果,不能把 InterruptedException 当作“没有发生副作用”的证明。相关事务与结果未知见从一次写入到一致状态。
执行器关闭与资源归还
ExecutorService的 shutdown 停止接受新任务,并允许既有任务继续执行;shutdownNow 尝试停止正在执行的任务,同时返回未开始的队列任务。两者都不能安全强杀任意业务代码。
关闭服务时先停止入口、定时提交者和其他生产者,再关闭它们拥有的执行器。共享执行器不能由某个普通调用者关闭。等待上限应纳入整个服务的终止预算,不能让每个组件重新消耗完整的退出时间。
队列任务的 Future 也要有去向
对于 ThreadPoolExecutor,shutdownNow 从队列排出的任务不会因此自动把对应 Future 全部改为取消。如果调用方还在 get,忘记处理这些结果句柄可能留下永久等待。
executor.shutdown();
if (!executor.awaitTermination(graceNanos, TimeUnit.NANOSECONDS)) {
List<Runnable> notStarted = executor.shutdownNow();
for (Runnable task : notStarted) {
if (task instanceof Future<?> future) {
future.cancel(false);
} else {
recordUnstarted(task);
}
}
boolean stopped = executor.awaitTermination(forceNanos, TimeUnit.NANOSECONDS);
if (!stopped) {
reportStillRunningTasks();
}
}这是执行器所有者的关闭骨架。graceNanos 和 forceNanos 来自已分配的剩余预算,recordUnstarted/reportStillRunningTasks 由应用实现。如果中间等待被中断,仍要执行必要的 shutdownNow、处理返回任务,然后传播 InterruptedException 或在退出边界恢复标志。
对于被包装、装饰或跨系统投递的任务,队列里的 Runnable 未必就是调用者拿到的 Future;应维护任务登记关系。记录“未开始”也不代表可以随意重投,重新执行外部写入仍受业务幂等与授权约束。
运行队列排空实验:
"$JAVA_HOME/bin/java" -cp "$LAB_OUT" CancellationLab shutdowndrained=1 queued-cancelled=true worker-cleaned=true terminated=true实验占住唯一 worker,把第二个任务放入队列。先 shutdown 后短暂等待,仍有任务未结束;shutdownNow 发出中断并返回恰好一个未启动任务。代码先确认排出的 Future 尚未完成,再显式 cancel(false),最后确认 worker 清理与执行器终止。它把队列移除、结果取消和执行完成分别检查。
JDK 19 起 ExecutorService 实现 AutoCloseable;close 会等待执行器终止,没有接受“最多等待多久”的参数。try-with-resources 能保证调用 close,却不能把一个不合作的任务变成有界关闭。需要严格停止预算时,使用明确的分阶段关闭,而不是仅靠作用域语法。
清理也需要可结束
finally 和 try-with-resources 能覆盖 Java 的正常返回与异常展开,但进程被 SIGKILL、Runtime.halt 或机器故障终止时,清理代码未必有机会执行。清理方法自己若无限等待另一个线程或下游,也会拖住任务退出。
资源应在成功取得之后建立对应释放责任:已取得的 permit 必须释放,未取得的不能释放;属于当前任务的连接应归还,共享连接池则不应被任务关闭。锁的 unlock 放在成功获得锁后的 finally 中。需要可靠恢复的外部写入,应把可恢复状态存到业务系统,不把 Java finally 当作持久化协议。
排查任务无法结束
区分结果取消与执行停滞
先记录任务开始、取消请求、进入清理和清理完成的位置。线程池里还要区分排队任务、正在运行的任务与已完成结果;单看 Future.isCancelled 无法算出仍在占用资源的任务数。
| 已观察现象 | 优先检查 | 修复与复验 |
|---|---|---|
| Future 已取消,任务仍活跃 | 检查点、吞掉中断的 catch、下游超时 | 修复合作退出;确认取消后活动任务和资源占用回落 |
| 高 CPU,反复进入阻塞异常处理 | 是否恢复标志后又回到原等待循环 | 退出或向上传播;同负载下 CPU 与异常计数下降 |
| 长期 BLOCKED | monitor 持有者、临界区和锁顺序 | 缩短持锁;需要可取消获取时改用合适的 Lock API |
| 任务体已结束,关闭仍未返回 | finally、close、其他执行器或非 daemon 线程 | 为真实资源操作设定预算;核对最后未结束对象 |
| shutdownNow 后调用方一直 get | 队列任务是否已排出但 Future 未完成 | 处理未开始任务的句柄;断言每个等待者获得明确结果 |
| 入口超时后仍发生写入 | 任务是否继续、远端是否已接收或提交 | 查幂等记录/业务状态;不靠重发 interrupt 推断回滚 |
需要线程栈时,在目标 Linux 主机或容器内,以 Java 进程所属用户执行。下列 PID 来自实际目标,不使用前面已经退出的演示进程;完整权限、命名空间和采集影响说明见JVM 综合诊断。
read -r -p "输入仍在运行的 Java PID: " PID
case "$PID" in
""|*[!0-9]*|0) printf 'PID 无效\n' >&2; exit 1 ;;
esac
ps -o pid,user,etime,cmd -p "$PID"
umask 077
CASE_DIR=$(mktemp -d /var/tmp/cancel-case.XXXXXX)
"$JAVA_HOME/bin/jcmd" "$PID" help Thread.print
"$JAVA_HOME/bin/jcmd" "$PID" Thread.print -l >"$CASE_DIR/threads.txt"
grep -n -A25 'cancel-\|report-worker\|java.lang.Thread.State' "$CASE_DIR/threads.txt"Thread.print 有采集成本,线程多时尤其要评估。栈中停在计算循环,回查取消检查点;停在驱动或 native 调用,核对该操作的超时与关闭规则;停在 monitor,找同一个锁的 owner。一次快照不能读出所有业务生命周期,线程转储也不能可靠替代任务对中断标志的显式观测。
多线程互相等待、同池子任务无法得到 worker 等情况,需要画出等待关系,见并发诊断。在根因未明确时,限流、摘流或重启只是止损;修复后要用相同任务类型、排队规模和下游延迟复测取消到退出的耗时,并核对资源归还。
运行全部模式并结束实验
源码包的 run.sh 对每个模式设置进程级时限,编译失败或结果不满足断言时非零退出。它需要 Bash、完整 JDK 和 GNU coreutils 的 timeout:
bash run.sh也可以从已解压目录,在已获 Docker 权限的 Linux 普通用户下运行。镜像需提前从可信来源获取;离线环境先由联网环境导出并核对镜像归档,再 docker load 导入。容器中的用户显式采用宿主 UID/GID,源码只读,编译结果写入临时目录;Docker daemon 的权限由主机配置决定。
docker run --rm --network none --read-only \
--user "$(id -u):$(id -g)" --cap-drop ALL --security-opt no-new-privileges \
--mount "type=bind,src=$(pwd),dst=/lab,readonly" \
--tmpfs /tmp:rw,nosuid,nodev,size=128m,mode=1777 \
eclipse-temurin:25.0.4_7-jdk bash /lab/run.sh成功时每个模式给出对应状态,最后打印 PASS cancellation-lab。握手超时、线程没有退出或输出断言失败时保留异常,先查 CPU 配额、JDK 与实际源码,不无限延长等待掩盖错误。实验没有性能排名,也不把两秒握手上限当成生产取消 SLA。
手动实验结束后,确认已没有本轮 Java 进程,再删除本轮编译目录。生产诊断文件可能含敏感数据,应按受控留存流程处理,不随源码清理:
case "$LAB_OUT" in
/tmp/cancellation-learning.*) rm -r -- "$LAB_OUT" ;;
*) printf '保留未知目录,不执行清理\n' >&2 ;;
esac
unset LAB_OUT权威资料与规范地址
线程生命周期、阻塞行为与取消实现分别查对应 API 或固定源码版本。
| 资料 | 完整地址 |
|---|---|
| Thread 生命周期与中断 | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Thread.html |
| 六种线程状态 | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Thread.State.html |
| JLS happens-before | https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.4 |
| JLS 等待与通知 | https://docs.oracle.com/javase/specs/jls/se25/html/jls-17.html#jls-17.2 |
| Object.wait | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#wait() |
| LockSupport | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/LockSupport.html |
| InterruptibleChannel | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/channels/InterruptibleChannel.html |
| Future | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/Future.html |
| OpenJDK FutureTask 实现 | https://github.com/openjdk/jdk/blob/jdk-25%2B36/src/java.base/share/classes/java/util/concurrent/FutureTask.java |
| CompletableFuture.cancel | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/CompletableFuture.html#cancel(boolean) |
| System.nanoTime | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/System.html#nanoTime() |
| ExecutorService | https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/ExecutorService.html |
