首页 > 其他分享 >原来这才是 JDK 推荐的线程关闭方式,别再乱用了!

原来这才是 JDK 推荐的线程关闭方式,别再乱用了!

时间:2023-12-04 15:15:31浏览次数:32  
标签:Thread JDK 中断 interrupt 线程 关闭 退出

原文:juejin.cn/post/7291564831710445622

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

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

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

需要线程退出的常见场景

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

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

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

推荐一个开源免费的 Spring Boot 实战项目:

https://github.com/javastacks/spring-boot-best-practice

优雅关闭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方法,

  1. 恰好目标线程在调用. Object.wait(),object.join(),Object.sleep()等方法时,目标线程的中断位标记被清除,同时目标线程会立即从sleep、wait等调用中恢复,并且被抛出InterruptException。
  2. 如果目标线程在IO操作中被阻塞,例如io.channels.InterruptibleChannel,Channel将被关闭,线程的中断位被设置,同时目标线程收到java.nio.channels.ClosedByInterruptException。
  3. 如果目标线程被阻塞在java.nio.channels.Selector,线程中断状态被设置,然后目标线程立即从select中返回非零值。
  4. 如果其他条件都不成立,该线程中断位会被设置。

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

推荐一个非常好的博客 通过JVM源码分析 interrupt和sleep的实现原理。

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

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

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

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

推荐的中断响应策略

立即响应中断
  1. 目标线程的任务在InterruptedException异常处理中,要主动回收资源,打印日志,退出任务执行。
  2. 目标线程如果没有阻塞操作,例如sleep、wait。可以通过 Thread.isInterrupted(),查看当前中断位状态,如果被中断了,则采取以上第一步操作。
忽略中断,交给上一层处理

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

  1. 抛出InterruptedException给上层,由上层代码处理。
  2. 调用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()。

总结

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

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2022最新版)

2.劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

标签:Thread,JDK,中断,interrupt,线程,关闭,退出
From: https://www.cnblogs.com/javastack/p/17874961.html

相关文章

  • 解决线程不安全
    1.破坏临界资源(临界资源破坏了原子性可见性有序性) 直接不使用临界资源2.只读  使用final,只读不写3.局部变量  每个线程的局部变量会存在栈帧中,会在每个线程的栈帧内存中被创建多份,因此不存在共享。ThreadLocal  ThreadLocal也就是线程本地变量。如果你创建了⼀......
  • 线程安全
    什么是线程安全:⼤⽩话:多线程下并发同时对共享数据进⾏读写,会造成数据混乱=线程不安全当多线程并发访问临界资源时,如果破坏其原⼦性、可⻅性、有序性,可能会造成数据不⼀致。临界资源:共享资源(同⼀对象)同时读写,⼀次仅允许⼀个线程使⽤,才可保证其正确性。原子性:单一,不可分割......
  • Python 多线程
    多线程类似于同时执行多个不同程序,多线程运行有如下优点:使用线程可以把占据长时间的程序中的任务放到后台去处理。用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度程序的运行速度可能加快在一些等待的任务实现上如用户输......
  • C++-Python_多进程_多线程-协程-异步开发
    python多任务、并发编程等领域并发:宏观上并行,微观上串行并行:宏观上并行,微观上并行并发:I/O密集型作业运行态---阻塞态的转化并行:CPU密集型作业并发(Concurrent)、并行(Parallesim)、多线程(MultiThreaded)、多进程(Multiprocessing)、多任务(Multitasking)、......
  • Jdk升级到1.7后,hutool工具类的很多方法报错解决方案
    报错如下:java.lang.reflect.InaccessibleObjectException:Unabletomakefieldprivatefinaljava.util.Mapsun.reflect.annotation.AnnotationInvocationHandler.memberValuesaccessible:modulejava.basedoesnot"openssun.reflect.annotation"tounnamed......
  • IDEA2023只能创建jdk17和21的springboot项目解决
    现象:解决:将serverurl修改即可https://start.spring.io--》https://start.aliyun.com可以对比下内容修改成功后就可以创建低版本jdk的springboot项目了......
  • Tomcat 配合虚拟线程,一种新的编程体验
    Java21在今年早些时候的9月19日就正式发布,并开始正式引入虚拟线程,但是作为Java开发生态中老大哥Spring并没有立即跟进,而是在等待了两个月后的11月29日,伴随着SpringBoot3.2版本的发布,在这个版本中也终于是引入了对虚拟线程的支持。虚拟线程的引入标志着Java在......
  • 线程淘汰策略
    1、线程池的拒绝策略  等待队列已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,⽆法继续为新任务服务。这个是时候我们就需要拒绝策略机制合理的处理这个问题。2、JDK内置的拒绝策略  AbortPolicy(默认):直接抛出RejectedExecutionException异常阻⽌系统正常运......
  • 内置线程池的使用
    packageorg.example.c2;importlombok.extern.slf4j.Slf4j;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.ScheduledExecutorService;importjava.util.concurrent.TimeUnit;/****@Author徐庶......
  • Java线程安全问题
    一、共享资源共享资源是指,同时会有多个线程访问的资源。二、线程安全问题线程安全问题是指多个线程同时读写共享资源时并且没有任何同步措施的情况下,出现脏数据或者其他不可预见的结果的问题。当然如果所有线程都只是读取共享资源而不去修改共享资源是不会出现线程安全问题的。......