—— 线程中断如何让任务安全停止)
线程停止不是一个简单的 API 调用问题。真正的问题是一个线程可能正在修改共享状态可能正在持有锁可能正在阻塞等待队列、I/O 或其他资源。如果外部直接把线程强行终止线程可能停在任意位置留下半完成状态。Java 不推荐强制停止线程而是提供了中断机制让外部线程发出停止请求再由任务自己在合适的位置退出。中断的核心不是“杀死线程”而是“通知线程该停止了”。线程是否退出、什么时候退出、退出前是否清理资源都取决于任务代码本身是否写好了响应路径。1.interrupt()只是发出停止请求调用一个线程的interrupt()方法并不会强制终止这个线程worker.interrupt();它做的事情可以先理解为把目标线程的中断标记设置为true。这个标记表示“有人希望你停下来”但线程不会因为这个标记自动退出。所以中断是一种协作式停止机制外部线程只负责发出请求Worker 线程要自己读取这个请求并决定是否退出。2. 运行中的任务要主动检查中断标记如果任务一直在运行没有进入阻塞方法那么它必须主动检查中断标记。常见写法是classWorkerimplementsRunnable{Overridepublicvoidrun(){while(!Thread.currentThread().isInterrupted()){doWork();}cleanup();}privatevoiddoWork(){// 执行一小段可重复的任务}privatevoidcleanup(){// 释放资源、记录日志等}}这里真正让任务退出的不是interrupt()而是循环条件while(!Thread.currentThread().isInterrupted())当其他线程调用worker.interrupt()后Worker 下一次执行isInterrupted()时会看到中断标记已经变成true循环结束任务自然退出。这种写法适合计算型任务、轮询型任务或者每次循环都能比较快回到判断条件的位置。如果循环体内部长期不返回或者卡在某个阻塞方法里单靠循环条件就不够了。3. 阻塞中的任务通过InterruptedException被唤醒任务不一定一直处于运行状态。更常见的情况是Worker 等待任务队列Tasktaskqueue.take();如果队列为空线程会阻塞在take()里面。此时它回不到while条件也就没有机会主动执行isInterrupted()。这时需要阻塞方法本身支持中断。例如BlockingQueue.take()的方法签名包含Etake()throwsInterruptedException;这表示线程阻塞在take()时如果外部调用interrupt()take()会被唤醒并抛出InterruptedException。完整的 Worker 写法通常是classQueueWorkerimplementsRunnable{privatefinalBlockingQueueTaskqueue;QueueWorker(BlockingQueueTaskqueue){this.queuequeue;}Overridepublicvoidrun(){try{while(!Thread.currentThread().isInterrupted()){Tasktaskqueue.take();process(task);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}finally{cleanup();}}privatevoidprocess(Tasktask){// 处理任务}privatevoidcleanup(){// 关闭连接、释放资源、写日志等}}这里的结构分成四层while负责运行状态下检查中断queue.take()负责阻塞状态下响应中断catch负责处理中断finally负责退出前清理资源。需要注意try-catch不是让中断生效的原因。中断能唤醒take()是因为take()本身支持中断try-catch只是因为InterruptedException是受检异常代码必须处理它。4.catch里不能吞掉中断阻塞方法因为中断抛出InterruptedException后通常会清除当前线程的中断标记。也就是说进入catch时中断标记可能已经变回false。因此常见写法是catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}这两句的职责不同Thread.currentThread().interrupt();负责恢复中断标记表示“当前线程确实被中断过”。return;才是真正让当前任务退出的语句。不要写成下面这样try{Tasktaskqueue.take();process(task);}catch(InterruptedExceptione){// 什么都不做}这种写法会吞掉中断。外部线程已经发出停止请求阻塞方法也已经被唤醒但任务没有退出也没有恢复中断标记停止请求就丢失了。如果当前方法不能直接退出而是要把中断交给上层处理也应该恢复中断标记catch(InterruptedExceptione){Thread.currentThread().interrupt();}这样上层代码仍然可以通过isInterrupted()看到当前线程曾经被请求停止。5.isInterrupted()和Thread.interrupted()不一样读取中断标记有两个常见方法方法作用对象是否清除中断标记常见用途thread.isInterrupted()指定线程否普通循环检查Thread.interrupted()当前线程是检查并消费中断信号isInterrupted()只读取标记不清除Thread.currentThread().interrupt();System.out.println(Thread.currentThread().isInterrupted());// trueSystem.out.println(Thread.currentThread().isInterrupted());// trueThread.interrupted()会读取当前线程的中断标记并把它清除Thread.currentThread().interrupt();System.out.println(Thread.interrupted());// trueSystem.out.println(Thread.interrupted());// false所以在任务循环中通常使用while(!Thread.currentThread().isInterrupted()){doWork();}不优先使用while(!Thread.interrupted()){doWork();}因为后者会把中断标记清掉后面的代码可能就不知道当前线程被中断过了。6. 调用线程如何安全关闭线程池线程池里的任务通常不是直接通过Thread对象管理而是通过Future或ExecutorService控制。虽然 API 不同但底层思想仍然是中断 Worker 线程。Future.cancel(true)表示如果任务还没有开始就取消它如果任务已经在运行就尝试中断执行它的线程。Future?futureexecutor.submit(newQueueWorker(queue));future.cancel(true);这里的true不是强制终止任务而是允许对正在执行任务的 Worker 线程调用interrupt()。任务能不能停止仍然取决于QueueWorker内部是否按照前文方式响应中断。线程池关闭时常见做法是先温和关闭再超时升级executor.shutdown();try{if(!executor.awaitTermination(30,TimeUnit.SECONDS)){executor.shutdownNow();}}catch(InterruptedExceptione){executor.shutdownNow();Thread.currentThread().interrupt();}这段代码中调用线程和 Worker 线程承担不同职责。shutdown()表示线程池不再接收新任务但已经提交的任务继续执行awaitTermination()是调用线程等待线程池结束如果等待超时再调用shutdownNow()尝试中断正在执行任务的 Worker 线程。如果调用线程自己在awaitTermination()期间被中断就进入catch。此时调用线程不应该继续等待而是升级为shutdownNow()并恢复自己的中断标记catch(InterruptedExceptione){executor.shutdownNow();Thread.currentThread().interrupt();}这里恢复的是调用线程的中断状态不是 Worker 线程的中断状态。shutdown()和shutdownNow()的区别可以概括如下方法是否接收新任务队列中未开始任务正在执行的任务shutdown()否继续执行继续执行shutdownNow()否从队列移除并返回尝试中断shutdownNow()返回的是队列里还没开始执行的任务不包含已经被 Worker 线程取走、正在执行中的任务。正在执行的任务只会收到中断请求是否退出仍然取决于任务代码。7.interrupt()不能唤醒所有等待不是所有阻塞都能被interrupt()唤醒。像下面这些方法通常支持中断方法中断后的表现Thread.sleep()抛出InterruptedExceptionObject.wait()抛出InterruptedExceptionThread.join()抛出InterruptedExceptionBlockingQueue.take()抛出InterruptedExceptionCondition.await()抛出InterruptedExceptionReentrantLock.lockInterruptibly()抛出InterruptedException但线程等待进入synchronized锁时不能被interrupt()唤醒。synchronized(lock){doWork();}如果线程正在等待进入这个同步块外部调用interrupt()只会设置它的中断标记线程仍然会继续等待锁。如果希望“等待锁时也能响应中断”可以使用ReentrantLock.lockInterruptibly()classInterruptibleLockWorkerimplementsRunnable{privatefinalReentrantLocklock;InterruptibleLockWorker(ReentrantLocklock){this.locklock;}Overridepublicvoidrun(){try{lock.lockInterruptibly();try{doWork();}finally{lock.unlock();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}}privatevoiddoWork(){// 持锁执行临界区逻辑}}这里讨论的是“还没拿到锁正在等锁”的阶段。如果线程已经拿到锁并进入临界区中断仍然不会强行把它赶出去。它仍然要执行完当前临界区再根据任务代码决定是否退出。8. 为什么不用Thread.stop()Thread.stop()的问题是强制终止线程。它不给线程自己选择退出位置的机会线程可能停在任意一行代码上。例如classAccount{privateintbalance;privateintfrozenAmount;publicsynchronizedvoidtransferOut(intamount){balance-amount;frozenAmountamount;}}这两个字段更新本来应该一起完成。如果线程执行完第一句后被Thread.stop()强行终止balance-amount;// 线程在这里被强制终止frozenAmountamount;对象就会处于半完成状态balance已经减少但frozenAmount还没有增加。更危险的是线程被强行终止时会释放已经持有的锁其他线程随后可以进入同步方法看到这个不一致状态。interrupt()不会这样做。它只是设置中断标记线程仍然可以完成当前临界区、执行finally、释放资源然后在自己可控的位置退出。9.volatile running和interrupt()的选择有些任务会使用一个volatile标记控制停止classRunningWorkerimplementsRunnable{privatevolatilebooleanrunningtrue;publicvoidstop(){runningfalse;}Overridepublicvoidrun(){while(running){doWork();}}privatevoiddoWork(){// 非阻塞任务}}这种写法适合非阻塞循环。只要 Worker 一直能回到while (running)它就能看到running false然后退出。但如果任务可能阻塞volatile running就不够while(running){Tasktaskqueue.take();process(task);}当线程阻塞在queue.take()时即使其他线程把running改成falseWorker 也没有机会回到循环条件重新读取它。相比之下interrupt()不但能设置中断标记还能唤醒支持中断的阻塞方法。因此选择规则可以整理为场景推荐方式原因简单非阻塞循环volatile running或interrupt()线程能主动检查停止条件可能进入sleep()/wait()/take()interrupt()可以唤醒可中断阻塞等待synchronized锁interrupt()不够只能设置标记不能唤醒锁等待等待ReentrantLock且需要可中断lockInterruptibly()等锁时可响应中断线程池任务取消cancel(true)/shutdownNow()本质是中断 Worker 线程10. 安全停止不是一个 API而是一组退出路径一个可安全停止的任务通常同时具备三部分代码。第一运行中能检查中断while(!Thread.currentThread().isInterrupted()){doWork();}第二阻塞中能响应中断try{Tasktaskqueue.take();process(task);}catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}第三退出前能清理资源finally{cleanup();}把它们合在一起才是一段完整的可停止 WorkerclassStoppableWorkerimplementsRunnable{privatefinalBlockingQueueTaskqueue;StoppableWorker(BlockingQueueTaskqueue){this.queuequeue;}Overridepublicvoidrun(){try{while(!Thread.currentThread().isInterrupted()){Tasktaskqueue.take();process(task);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();return;}finally{cleanup();}}privatevoidprocess(Tasktask){// 处理一个任务单元}privatevoidcleanup(){// 释放资源}}与之配合的调用线程负责发出停止请求ExecutorServiceexecutorExecutors.newFixedThreadPool(4);executor.submit(newStoppableWorker(queue));executor.shutdown();try{if(!executor.awaitTermination(30,TimeUnit.SECONDS)){executor.shutdownNow();}}catch(InterruptedExceptione){executor.shutdownNow();Thread.currentThread().interrupt();}前一段代码解决的是 Worker 如何响应中断后一段代码解决的是调用线程如何关闭线程池。只有两边配合线程池关闭才不是单纯“发一个信号”而是能让任务有机会自然退出、被中断唤醒、清理资源并把中断状态正确传递给上层。总结本章的因果链条可以从“不能强杀线程”开始理解因为线程可能正在持锁、修改共享状态或等待资源所以外部直接终止会破坏对象一致性因此 Java 更推荐用interrupt()发出停止请求。请求发出后如果任务仍在运行就要靠isInterrupted()主动检查如果任务正在可中断阻塞就要靠InterruptedException被唤醒异常被捕获后中断标记可能已经被清除所以需要恢复中断状态再通过return、break或循环结束真正退出。线程池中的cancel(true)和shutdownNow()只是把这套机制包装了一层它们负责发出中断请求但不保证任务一定停止。真正的安全停止最终仍然取决于任务是否在运行、阻塞、等锁和退出清理这些位置都写好了响应路径。