Day854.两阶段终止模式 -Java 并发编程实战

news2025/4/12 6:54:27

两阶段终止模式

Hi,我是阿昌,今天学习记录的是关于两阶段终止模式的内容。

从纯技术的角度看,都是启动多线程去执行一个异步任务。既启动,那又该如何终止呢?

那如何优雅地终止线程,正所谓有始有终。在Java 线程的生命周期中,线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。

不过“优雅地终止线程”,不是自己终止自己,而是在一个线程 T1 中,终止线程 T2;
这里所谓的“优雅”,指的是给 T2 一个机会料理后事,而不是被一剑封喉。

Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,可是早已不建议使用了,原因是这个方法用的就是一剑封喉的做法,被终止的线程没有机会料理后事。

既然不建议使用 stop() 方法,那在 Java 领域,又该如何优雅地终止线程呢?


一、如何理解两阶段终止模式

前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式

顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令

两阶段终止模式示意图
那在 Java 语言里,终止指令是什么呢?

这个要从 Java 线程的状态转换过程说起。

在Java 线程的生命周期中提到过 Java 线程的状态转换图,如下图所示。

Java 中的线程状态转换图
从这个图里你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态

如何做到呢?这个要靠 Java Thread 类提供的 interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。线程转换到 RUNNABLE 状态之后,如何再将其终止呢?

RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。这个过程其实就是前面提到的第二阶段:响应终止指令。

综合上面这两点,能总结出终止指令,其实包括两方面内容:interrupt() 方法和线程终止的标志位。


二、用两阶段终止模式终止监控操作

实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。

出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。

动态采集功能示意图

下面的示例代码是监控代理简化之后的实现,start() 方法会启动一个新的线程 rptThread 来执行监控数据采集和回传的功能,stop() 方法需要优雅地终止线程 rptThread,那 stop() 相关功能该如何实现呢?

class Proxy {
  boolean started = false;
  //采集线程
  Thread rptThread;
  //启动采集功能
  synchronized void start(){
    //不允许同时启动多个采集线程
    if (started) {
      return;
    }
    started = true;
    rptThread = new Thread(()->{
      while (true) {
        //省略采集、回传实现
        report();
        //每隔两秒钟采集、回传一次数据
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {  
        }
      }
      //执行到此处说明线程马上终止
      started = false;
    });
    rptThread.start();
  }
  //终止采集功能
  synchronized void stop(){
    //如何实现?
  }
}  

按照两阶段终止模式,首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要在调用 rptThread.interrupt() 就可以了。

线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,选择的标志位是线程的中断状态:

Thread.currentThread().isInterrupted() ,需要注意的是,在捕获 Thread.sleep() 的中断异常之后,通过 Thread.currentThread().interrupt() 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。

class Proxy {
  boolean started = false;
  //采集线程
  Thread rptThread;
  //启动采集功能
  synchronized void start(){
    //不允许同时启动多个采集线程
    if (started) {
      return;
    }
    started = true;
    rptThread = new Thread(()->{
      while (!Thread.currentThread().isInterrupted()){
        //省略采集、回传实现
        report();
        //每隔两秒钟采集、回传一次数据
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          //重新设置线程中断状态
          Thread.currentThread().interrupt();
        }
      }
      //执行到此处说明线程马上终止
      started = false;
    });
    rptThread.start();
  }
  //终止采集功能
  synchronized void stop(){
    rptThread.interrupt();
  }
}

上面的示例代码的确能够解决当前的问题,但是建议在实际工作中谨慎使用。

原因在于很可能在线程的 run() 方法中调用第三方类库提供的方法,而没有办法保证第三方类库正确处理了线程的中断异常,例如第三方类库在捕获到 Thread.sleep() 方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。

所以强烈建议设置自己的线程终止标志位,例如在下面的代码中,使用 isTerminated 作为线程终止标志位,此时无论是否正确处理了线程的中断异常,都不会影响线程优雅地终止。

class Proxy {
  //线程终止标志位
  volatile boolean terminated = false;
  boolean started = false;
  //采集线程
  Thread rptThread;
  //启动采集功能
  synchronized void start(){
    //不允许同时启动多个采集线程
    if (started) {
      return;
    }
    started = true;
    terminated = false;
    rptThread = new Thread(()->{
      while (!terminated){
        //省略采集、回传实现
        report();
        //每隔两秒钟采集、回传一次数据
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          //重新设置线程中断状态
          Thread.currentThread().interrupt();
        }
      }
      //执行到此处说明线程马上终止
      started = false;
    });
    rptThread.start();
  }
  //终止采集功能
  synchronized void stop(){
    //设置中断标志位
    terminated = true;
    //中断线程rptThread
    rptThread.interrupt();
  }
}

三、如何优雅地终止线程池

Java 领域用的最多的还是线程池,而不是手动地创建线程。

那该如何优雅地终止线程池呢?线程池提供了两个方法:shutdown()shutdownNow()

这两个方法有什么区别呢?要了解它们的区别,就先需要了解线程池的实现原理。

曾经讲过,Java 线程池是生产者 - 消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从阻塞队列中取出任务执行。

  • shutdown() 方法是一种很保守的关闭线程池的方法。线程池执行 shutdown() 后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。
  • shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为 shutdownNow() 方法的返回值返回。因为 shutdownNow() 方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。

如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。不过,如果提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线程池的。《Java 并发编程实战》这本书第 7 章《取消与关闭》的“shutdownNow 的局限性”一节中,提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行的方案,可以参考一下,方案很简单,这里就不详细介绍了。

其实分析完 shutdown() 和 shutdownNow() 方法会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。


四、总结

两阶段终止模式是一种应用很广泛的并发设计模式,在 Java 语言中使用两阶段终止模式来优雅地终止线程,需要注意两个关键点:

  • 一个是仅检查终止标志位是不够的,因为线程的状态可能处于休眠态;
  • 另一个是仅检查线程的中断状态也是不够的,因为我们依赖的第三方类库很可能没有正确处理中断异常。

当你使用 Java 的线程池来管理线程的时候,需要依赖线程池提供的 shutdown() 和 shutdownNow() 方法来终止线程池。

不过在使用时需要注意它们的应用场景,尤其是在使用 shutdownNow() 的时候,一定要谨慎。


本文的示例代码中,线程终止标志位 isTerminated 被声明为 volatile,你觉得是否有必要呢?


class Proxy {
  //线程终止标志位
  volatile boolean terminated = false;
  ......
}

stop和start方法对于terminated访问由于syn关键字,线程安全,但是start中新起了一个线程rptthread,导致stop方法中对于terminated存在可见性问题,因此需要volatie,原子性问题对这个代码段没有影响,所以原子性问题无需关注。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/151515.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka 概念

Kafka 属于分布式的消息引擎系统,主要功能 :提供一套完备的消息发布与订阅解决方案 生产者和消费者都是客户端(Clients): 生产者(Producer):向主题发布消息的客户端应用程序消费者…

前端基础(九)_this基本使用、this指向判断、改变this指向的方法

前言&#xff1a; 看这个例子&#xff1a; 例&#xff1a;给所有的div标签都添加点击事件&#xff0c;并打印当前节点的内容 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compa…

HW14基于规则化的终身学习Regularization-based Lifelong Learning

文章目录一、介绍-终身学习目标数据集任务示例代码-准备数据模型体系结构样本代码-训练和评估Training Pipeline:MAS - Memory Aware SynapseSISCP - Sliced Cramer Preservation二、实验1、baseline2、EWC3、MAS4、SI5、RWalk6、SCP一、介绍-终身学习 目标 一个模型可以打败…

Naive UI 组件使用体验之-级联选择 Cascader

使用场景 地址区域选择 简单使用 安装依赖 npm install naive-ui -D 按需引入之-手动引入 import {NCascader,CascaderOption } from naive-ui 使用 <n-cascader:value"regionValue":options"regionOptions":multiple"false"placehol…

一篇搞懂Python中的随机数

在 python 中生成随机样本的所有你需要的示例列表 长按关注《Python学研大本营》&#xff0c;加入读者群&#xff0c;分享更多精彩 扫码关注《Python学研大本营》&#xff0c;加入读者群&#xff0c;分享更多精彩 在这篇博客中&#xff0c;我将演示如何根据不同的需求在python…

Docker三剑客——Docekr Swarm

目录 一、概述 二、Swarm 的核心概念 1. Node&#xff08;节点&#xff09; 2. Service&#xff08;服务&#xff09; 3. Task&#xff08;任务&#xff09; 三、Swarm 服务的运行部分 1. 工作节点 2. 服务、任务和容器 3. 任务和调度 4. 副本服务和全局服务 四、Swa…

3. 【prometheus 学习】prometheus数据类型

prometheus 四种数据类型学习及测试 安装 1&#xff09;从官网下载对应版本的二进制压缩包并解压 2&#xff09;新建prometheus.yml 文件&#xff0c;静态指定抓取本机的9091端口&#xff0c;设置采样时间2秒1次&#xff0c;方便一会测试。 scrape_configs:- job_name: defin…

单细胞UMAP图只标记特定细胞群、圈定细胞群及坐标轴修改

最近有小伙伴私信我&#xff0c;如下的单细胞UMAP图怎么做&#xff1f;是啥样的图呢&#xff1f;就是UMAP图上其他群是灰色的&#xff0c;只有某个群或者某几个群是标记颜色的&#xff0c;比较突出。其实很简单&#xff0c;一种办法可参考我们之前写过的ggplot做UMAP图&#xf…

uni-app使用蓝牙

目录 前言 连接蓝牙 开启蓝牙适配器 发现蓝牙 连接蓝牙 收发蓝牙数据 获取服务ID 获取特征值 读取蓝牙数据 写蓝牙数据 遇到的坑 获取serviceId的坑 特征值不支持读写 notify成功后立刻写蓝牙数据 工具方法 前言 原因是公司要搞个共享单车给内部员工使用&#…

32位浮点数表示方法

今天开始给大家介绍计算机组成原理课程&#xff0c;本文主要内容是32位浮点数表示方法。 一、32位浮点数构成 32位浮点数是计算机中常见的一种数据类型&#xff0c;该数占据32bit空间&#xff0c;可以表示较大范围内的整数和小数。32位浮点数由三部分组成&#xff0c;分别是符…

Java集合List、Set、Map底层源码解析

目录集合介绍MapHashMapLinkedHashMapHashTablePropertiesCollectionListArrayListVectorLinkedListSetHashSetLinkedHashSetTreeSet集合介绍 为什么使用集合&#xff1f; 当我们想要保存一组数据时&#xff0c;可以使用到的变量类型有集合和数组。那么就像说一下数组的局限性…

1.9

完善案例 ​ this.$nextTick(function () {this.$refs.input.focus()})}​ //点击编辑直接获取焦点 this.$refs.input.focus() //但是这种写法 由于input是用v-show控制的 所以执行了上面代码 改变了isEdit值 但是不会立马去重新解析模板 input框还没有出来 //将handleedit回调…

springboot+xxl-job初体验

在日常开发中经常遇到任务调度的需求&#xff0c;下面体验一下分布式调度xxl-job。 1、常见定时任务的实现方式 Java自带的java.util.Timer类ScheduledExcutorServiceSpringboot自带的EnableScheduling注解Quartz框架Elastic-job框架XXL-JOB框架 下面对xxl-job实现任务调度尝…

【TypeScript】TS进阶-函数重载(七)

&#x1f431;个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️作者简介&#xff1a;前端领域新星创作者、华为云享专家、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab;系列专栏&#xff…

一文读懂电商数字员工究竟是什么?| RPA铺第1期

百度的曦灵&#xff0c;中核的核小智、万科集团数字员工“崔筱盼”都曾在2022年度爆火&#xff0c;数字员工一夜之间成为众多行业追捧的对象&#xff0c;那么&#xff0c;他/她们究竟代表着什么&#xff0c;对企业而言&#xff0c;是噱头大于收益&#xff0c;还是收益大于噱头呢…

简单的客户端服务器(多线程)基于json通信

这是一个客户端和服务器通信的小项目&#xff0c;小编自己手写了一个json工具&#xff0c;测试比jsoncpp&#xff0c;protobuf "效率更高"&#xff08;浅拷贝&#xff09;&#xff0c;改写成深拷贝效率会急剧下降&#xff08;小编能力有限&#xff0c;知识点&#xf…

技术分享 | Spring Boot 异常处理

说到异常处理&#xff0c;我们都知道使用 try-catch 可以捕捉异常&#xff0c;可以 throws 抛出异常。那么在 Spring Boot 中我们如何处理异常&#xff0c;如何更优雅的处理异常&#xff0c;如何全局处理异常。是本章讨论解决的问题。Java 异常类首先让我们简单了解或重新学习下…

Android开发-AS学习(一)

完整项目最精简流程&#xff1a;Android <-> 接口 <-> 数据库Android开发工具&#xff1a;Android Studio项目目录信息&#xff1a;一、控件1.1 TextView基础属性描述layout_width组件的宽度layout_height组件的高度id为TextView设置一个组件idtext设置显示的文本内…

Linux 学习笔记(借鉴黑马程序员Linux课程)

Linux视频课程 简介 Linux诞生于1991年&#xff0c;由林纳斯托瓦兹在21岁时完成。此后成为最为流行的服务器操作系统之一。 Linux内核和系统发行版 由Linux系统内核和系统级应用程序两部分组成。 内核提供系统最核心的功能&#xff0c;如&#xff1a;调度CPU、调度内存、调…

【笑小枫的按步照搬系列】Redis多系统安装(Windows、Linux、Ubuntu)

笑小枫&#x1f495; 欢迎来到笑小枫的世界&#xff0c;喜欢的朋友关注一下我呦&#xff0c;大伙的支持&#xff0c;就是我坚持写下去的动力。 微信公众号&#xff1a;笑小枫 笑小枫个人博客&#xff1a;https://www.xiaoxiaofeng.com Windows 下安装 方式一&#xff1a;https…