原来,这才是 JDK 推荐的线程关闭方式

Java技术迷

共 8005字,需浏览 17分钟

 ·

2023-11-06 18:35

点击关注公众号,Java干货及时送达

JDK 在线程的 Stop 方法时明确不得强行销毁一个线程,要优雅的退出线程。


何谓优雅退出线程,即业务将进行中请求正确被处理,取消待执行请求,执行资源回收,最终 Thread Runable run 方法 return 结束执行。

首先问为什么要退出一个线程,再提问如何退出一个线程

需要线程退出的常见场景

  • 任务执行完成,或异常终止,任务认为无需再占用线程。
  • 线程池根据当前任务执行情况,伸缩线程池。当任务执行较少时,退出空闲的线程。
  • 服务或进程在关闭阶段,例如滚动发布时,需要退出线程、关闭线程池、关闭进程。
  • 定时任务、周期任务需要终止执行时,需要退出当前线程。或者退出当前任务的执行。

总之既然能创建一个线程,就会有退出一个线程的能力。也会有退出线程的场景。

关闭一个线程的方式分为两种类型:通知线程主动关闭和强行关闭销毁线程。

优雅关闭 or 强行关闭


实际上强行关闭一个线程,坏处很多,假如要释放分布式锁前,突然关闭线程,那么这个分布式锁就无法释放。导致后续正常请求加锁失败被阻塞,影响用户提单等。
强行关闭一个线程无异于给服务器直接断电。

其他语言和 Java 语言退出线程的方式

除了 Java 其他语言如何退出线程呢,实际上每一种实现方式都有。例如 C++ 中可以通过 ExitThread、TerminateThread 强行终止线程执行。linux 既提供了 pthread_exit C 语言系统调用强行关闭线程,也提供了 pthread_cancel 通知线程关闭等优雅退出方式。

Java 也分别提供优雅和强制两种退出方式,但是目前 JDK 中明确极不推荐强制中断线程,在 Thread。stop() 强制中断线程的注释中, JDK 这样解释

Thread.stop() 这种方法本身就是不安全的,Stop 一个线程会随之解锁这个线程所持有的监视器(可以理解为锁),如果受这些监视器(锁)保护的临界对象处在不一致状态,则其他线程可能会看到这些对象处于不一致状态,那么将导致未知的行为。对 Thread.Stop() 的调用应该被简单的代码代替,例如 修改一个变量,目标线程定期检查这个变量,有序从 run 方法 return 出来。如果目标线程在一个条件变量上 wait,则其他线程应该使用 interrupt 方法中断目标线程。

实际上关闭一个线程强行和通知是两种理念,即是否应该相信线程任务的开发者优雅的、快速的主动退出线程,而不是被其他线程强制终止。在 Java 中,退出线程的方式只有一种推荐,即优雅退出,并且 JDK 也给了建议,通过修改变量,由目标线程定期检查状态。或者通过 interrupt 中断方式通知目标线程。

下面我们探讨下如何优雅退出一个线程?

优雅退出线程

有哪些方式呢?

业务字段标记

业务系统经常遇到终止一个任务的诉求,例如系统中存在定时任务,例如外卖券包在过期后,未使用的金额,自动给用户退款。假设任务执行中,我需要重新制定任务的入参,需要先终止任务。如何做呢?大部分任务类代码都会循环处理,例如扫描全表执行某个业务逻辑。一定存在循环处理的场景,可以在循环入口处判断任务是否需要终止执行,这样通过控制这个字段,我们就可以终止任务执行。

具体实施时,可以通过配置中心控制某一个任务是否要终止。


while(config.isTaskEnable()) {    //从配置中心获取任务是否要终止    //循环执行业务逻辑。直到执行完成退出,或者被终止。}


这种退出方式,是告知线程 “你应该在合适时机退出”, 由线程自己选择在合适的时机检查该状态。那么开发者在设计任务代码时,就要提前设计 合理的退出点,在退出点检查是否需要退出。


Thread.interrupt()


JDK 中提到了如果目标线程没有处于运行态,而是处于阻塞状态,自然无法检查退出的状态标记,如何通知这个线程退出呢?


JDK: 如果目标线程在一个条件变量上 wait,则其他线程应该使用 interrupt 方法中断目标线程。


interrupt 的 JDK 注释提到,


如果其他线程调用目标线程的 interrupt 方法,


  • 恰好目标线程在调用。Object.wait(),object.join (),Object.sleep() 等方法时,目标线程的中断位标记被清除,同时目标线程会立即从 sleep、wait 等调用中恢复,并且被抛出 InterruptException。

  • 如果目标线程在 IO 操作中被阻塞,例如 io.channels.InterruptibleChannel,Channel 将被关闭,线程的中断位被设置,同时目标线程收到 java.nio.channels.ClosedByInterruptException。

  • 如果目标线程被阻塞在 java.nio.channels.Selector,线程中断状态被设置,然后目标线程立即从 select 中返回非零值。

  • 如果其他条件都不成立,该线程中断位会被设置。


线程中断位标记了当前线程是否处于被中断状态,并且提供了 Thread.isInterrupted 方法查看当前是否处于中断位?那为什么目标线程阻塞在 Object.wait(),Sleep() 方法时,抛出了 interruptException,会取消标记呢?实际上 interrupt 操作执行两件事,1)设置中断位标记 2)通过 unpark 唤醒目标线程(park 和 unpark 分别可以阻塞线程和唤醒线程)


然而目标线程醒来时会检查当前是否处于中断位,如果是 sleep 或者 wait 操作。如果处于中断位则取消中断位,抛出异常。取消中段位的原因应该是一种规范,即抛出中断异常,即通知了线程中断,无需再用中段位标记。


其他场景 2、场景 3 在被唤醒后,分别执行对应的中断响应策略。


interrupt 中断逻辑是确定的,业务线程要考虑自己是否调用了 sleep、wait 或者 io、selector 等操作,根据不同的场景,选择自己合适的中断响应策略。


那么推荐业务线程如何响应中断呢?


推荐的中断响应策略


立即响应中断


  • 目标线程的任务在 InterruptedException 异常处理中,要主动回收资源,打印日志,退出任务执行。

  • 目标线程如果没有阻塞操作,例如 sleep、wait。可以通过 Thread.isInterrupted(),查看当前中断位状态,如果被中断了,则采取以上第一步操作。


忽略中断,交给上一层处理


所谓上一层,可以理解为是调用堆栈的上一层,例如本层代码不负责处理中断这个场景,那么 Interrupt 异常被抛出后,可以选择如何方案:


  • 抛出 InterruptedException 给上层,由上层代码处理。

  • 调用 Thread.interrupt()。重新设置中断位标记 (自己中断自己)。由上游代码在本层方法返回后,检查中断位标记,进行中断处理。


当然最推荐的方式还是抛出 InterruptedException,让上游感知到下游调用链中存在阻塞,让上游对中断异常进行处理。


千万不要吞掉中断


什么是吞掉中断?例如当 sleep 抛出 InterruptedException 后,忽略异常,不执行任何操作,继续执行业务逻辑。


for (int i = 0; i < cnt; i++) {   try {      //执行业务逻辑      Thread.sleep(10000);   } catch (InterruptedException e) {
System.out.println("被中断"); } System.out.println("子线程执行中");}

如果这样处理,中断异常被忽略,中断标记位也被忽略。即便上游方法对中断有处理策略,也无法感知到中断。例如上游调用可能会判断。


while(true){    callChildMethod();//调用下游方法,但是下游吞掉了中断    if (Thread.currentThread().isInterrupted()) {       //回收资源,退出线程    }}

有人会问,既然上层都能知道处理中断,为什么下层方法开发者会不记得抛出中断或重置中断位呢?

因为上下两层,很可能不是一个开发者。例如上层是通用的框架代码,定义了任务的指定逻辑,提供了扩展点方法,下游只需要实现扩展方法即可。但是另一个开发者在实现扩展点方法时,吞掉了中断异常,导致本来框架层已经处理好中断了,但还是无法响应中断。

所以中断的响应是需要上下层,每一层代码逻辑都需要考虑的事情。就算框架层处理好中断异常处理,业务逻辑层也要关注中断处理。

最后提醒一下,Thread.interrupted 方法会返回当前中断标记,并且取消中断位。如果只查询中断位,不想清理,可以使用 Thread.isInterrupted()。

总结

  • 不推荐强制销毁线程,会导致资源无法被释放,进行中请求无法正常处理完,导致业务数据处于不可知的状态。
  • Java 推荐优雅退出线程。
  • 业务层可以使用字段标记,定期检查是否需要退出任务。
  • Thread.interrupt 中断目标线程、isInterrupted 查询中断位标记。
  • 使用 Thread.interrupt 处理中断也可以优雅退出,但需要上下层堆栈都要关注中断,不得吞掉中断。


转自:五阳神功,

链接:juejin.cn/post/7291564831710445622


    
    

  

       
       

        
        

1、华为/荣耀把谷歌当病毒杀了

2、MySQL到底是 join 性能好,还是in一下更快呢?

3、Next.js支持在前端代码中写SQL,开倒车还是遥遥领先?

4、为什么国外JetBrains做 IDE 就可以养活自己,国内不行?区别在哪?

5、相比高人气的Rust、Go,为何 Java、C 在工具层面进展缓慢?

6、互联网博物馆爆火,网友满满回忆杀,看看你能想起几个?

点在看

浏览 1508
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报