一条中断日志改变了退出条件:interrupted 与 isInterrupted 的副作用
外层循环准备检查中断并退出,辅助方法先打印了一次“当前已经中断”,外层却又继续执行。日志没有看错,问题是辅助方法调用了Thread.interrupted:它既读取当前线程的标记,也清除这份标记。一次诊断调用已经改变了控制流。
isInterrupted只查询目标线程,不做这次清除。两个名字很接近,写在日志参数里也都像布尔表达式,但它们并不是可随意互换的观察工具。谁有权消费取消信号,应当像谁有权修改任务状态一样明确。
一次只读诊断,不该悄悄变成确认消费
以下Java 17程序只在主线程上设置受控中断,没有真实业务任务。sleep的一毫秒用于提供可中断操作,不是测试取消延迟。保存为InterruptObservation.java,执行java InterruptObservation.java。
public class InterruptObservation {
static boolean legacyDiagnostic() { return Thread.interrupted(); }
static boolean passiveDiagnostic() { return Thread.currentThread().isInterrupted(); }
static void work() throws InterruptedException { Thread.sleep(1); }
public static void main(String[] args) throws Exception {
Thread current=Thread.currentThread();
try {
current.interrupt();
System.out.println("legacy observed="+legacyDiagnostic()+", outer flag="+current.isInterrupted());
work();
System.out.println("work after legacy diagnostic=continued");
current.interrupt();
System.out.println("passive observed="+passiveDiagnostic()+", outer flag="+current.isInterrupted());
try { work();throw new AssertionError("interrupt expected"); }
catch(InterruptedException expected) {
System.out.println("sleep threw=true, flag after exception="+current.isInterrupted());
current.interrupt();
}
System.out.println("outer sees restored="+current.isInterrupted());
if(!current.isInterrupted())throw new AssertionError("restoration lost");
Thread.interrupted();
Thread unrelated=new Thread(()->{},"unstarted-other");
current.interrupt();
boolean fromCurrent=unrelated.interrupted(); // Deliberately misleading static-call syntax.
System.out.println("instance syntax read current="+fromCurrent+", current flag="+current.isInterrupted());
System.out.println("unrelated flag="+unrelated.isInterrupted());
} finally { Thread.interrupted(); }
System.out.println("experiment left flag="+current.isInterrupted());
}
}
legacy observed=true, outer flag=false
work after legacy diagnostic=continued
passive observed=true, outer flag=true
sleep threw=true, flag after exception=false
outer sees restored=true
instance syntax read current=true, current flag=false
unrelated flag=false
experiment left flag=false
legacyDiagnostic看到true以后,外层标记已经false,后续sleep正常继续。passiveDiagnostic保留true,下一次sleep立即按中断合同抛InterruptedException;异常路径中标记被清除,catch重新设置后外层又能观察到取消请求。
这三次变化来自不同动作:查询并清除、可中断操作抛异常、调用方恢复标记。不能只看最后一次isInterrupted为false,就认定中断根本没有到达。诊断应记录在哪个调用点观察,尤其不要用有清除副作用的方法去证明标记一直存在。
程序最后清掉本实验留下的状态,让后续步骤不承接一个人为中断。这是测试自己拥有的状态,不是建议通用库函数在finally里无条件清理调用线程。生产代码是否清除,需要由取消协议的拥有者决定,而不是为了让一段日志更整齐。
静态方法不会因为写在对象后面就换目标
Thread.interrupted是静态方法,始终作用于当前执行线程。Java允许通过一个Thread实例写出调用语法,但那不会改成检查该实例代表的线程。示例用未启动的unrelated对象调用它,读到的仍然是主线程标记。
实际代码应始终写Thread.interrupted来突出静态含义。若要查询另一个线程,用target.isInterrupted;要查询当前线程而不消费,用Thread.currentThread().isInterrupted。先把操作对象与是否清除分清,日志才不会把“谁被取消”记录到另一条任务上。
辅助方法若只是为了给排障增加上下文,应选择无副作用查询。某个组件确实需要消费标记以完成自己实现的不可中断等待,也必须保存原状态,并按合同在退出时恢复或向上报告;不能让调用者在返回后完全不知道取消请求曾经发生。
catch以后怎么做,取决于方法的退出合同
方法本来允许throws InterruptedException时,通常让异常向上传播,由上层决定停止或清理。不能因为想少写一个throws,就在里面记录日志然后继续执行剩余工作;那会把明确的取消通道变成没有行为效果的提示。
若Runnable等接口不能继续抛出这项受检异常,可以恢复标记并结束当前工作,让外层调度边界知道任务因取消退出。恢复以后仍然无限循环尝试相同阻塞操作,也可能每次立刻抛异常,变成忙循环;恢复标记要与停止或明确移交控制权一起设计。
也不是所有最外层线程入口都必须为了形式而恢复。若该入口拥有取消政策,已经决定立即结束这条线程,并有明确结果记录,标记是否还要保留应由它的接口合同决定。关键是不把取消请求无声吞掉,而不是给每个catch机械加同一行代码。
清理动作同样需要范围。如果取消后还有一项必要释放,应该尽量短小并使用合适资源接口;不能先清掉中断,然后启动另一轮无上限网络等待,声称这叫“确保清理”。需要有限等待与降级时,应让清理阶段本身有独立而可解释的限制。
中断标记不会变成业务事务状态
观察到中断不等于外部操作已经撤回。某个请求可能在取消前已经到达远端,某个文件也可能已经写出一部分。退出前应保留必要执行结果或错误身份,重试再按业务幂等核对,不能单凭一份线程标记决定“肯定什么都没有发生”。
标记也不适合当作跨线程业务消息箱。它表达线程级取消或唤醒相关意图,不携带是哪一个订单失败、哪一批记录要重试。把多种业务状态全部编码成一次interrupt,后续即便正确保留标记,也无法恢复那些没有保存的信息。
线程池还会复用工作线程。任务应按所用执行器和阻塞接口的合同响应取消,不把清除或恢复写成影响下一条任务的隐含副作用。这里没有模拟某个线程池的内部清理策略,受控主线程实验只证明两种查询和sleep异常路径的明确差异。
回归样本应至少同时包含“诊断以后是否继续工作”和“外层还能否观察取消”,而不只是断言曾经打印true。这样以后把一条日志抽进公共工具类时,测试能发现它意外消费信号。观察本应帮助理解执行,不能让观察动作自己改写了被诊断的退出条件。