Java并发编程学习15-任务关闭(下)

2023-11-01

任务关闭(下)

《任务关闭》由于篇幅较多,拆分了两篇来介绍各种任务和服务的关闭机制,以及如何编写任务和服务,使它们能够优雅地处理关闭。
在这里插入图片描述

1. 处理非正常的线程终止

我们知道,当单线程的控制台程序由于发生了一个未捕获的异常而终止时,程序将停止运行,并在控制台输出该异常的栈追踪信息。

那如果并发程序中某个线程因为发生故障而终止,那应用程序会怎么样呢 ?

实际上虽然某个线程发生了故障了,但我们的应用程序可能仍然正常运行。即便在运行日志中可能会输出栈追踪信息,因为程序正常运行,我们也很难去关注到,从而这种失败很可能会被我们忽略掉。

那通常是什么原因导致线程终止的呢 ?

通常最主要的原因就是运行时异常【RuntimeException】。这一类异常由于表示出现了某种编程错误或者其他不可修复的错误,通常它们不会被程序捕获。它们也不会在调用栈中逐层传递,而是默认地在控制台中输出栈追踪信息,并终止线程。

线程非正常退出的后果可能是良性的,也可能是恶性的,这要取决于线程在应用程序中的作用。例如,如果在 GUI 程序中丢失了事件分派线程,那么应用程序将停止处理事件并且 GUI 程序会因此失去响应。

由于任何代码都可能抛出一个 RuntimeException。因此我们要特别注意,每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目地认为它一定会正常返回,或者一定会抛出在方法原型中声明的某个已检查异常。

下面我们来看一下如下的示例【典型的线程池工作者线程结构】

	public void run() {
		Throwable thrown = null;
		try {
			while (!isInterrupted())
				runTask(getTaskFromWorkQueue());
		} catch (Throwable e) {
			thrown = e;
		} finally {
			threadExited(this, thrown);
		}
	}

上述示例中,如果任务抛出一个未检查异常,那么它将使线程终结,但会首先通知框架该线程已经终结。然后,框架可能会用新的线程来代替这个工作者线程,也可能不会,因为线程池正在关闭,或者当前已有足够多的线程能满足需要。

ThreadPoolExecutorSwing 都通过这项技术来确保行为糟糕的任务不会影响到后续任务的执行。

1.1 未捕获异常的处理

上面我们介绍了一种主动方法来解决未检查异常,而在 Thread API 中同样提供了 UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。两者结合,能有效地防止线程泄露问题。

当一个线程由于未捕获异常而退出时,JVM 会把这个事件报告给应用程序提供的 UncaughtExceptionHandler 异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到 System.err

知识点:

  • Java 5.0 之前,控制 UncaughtExceptionHandler 的唯一方法就是对 ThreadGroup 进行子类化。
  • Java 5.0 及之后 的版本中,可以通过 Thread.setUncaughtExceptionHandler 为每个线程设置一个 UncaughtExceptionHandler,还可以使用 setDefaultUncaughtExceptionHandler 来设置默认的 UncaughtExceptionHandler
  • 在这些异常处理器中,只有其中一个将被调用 — JVM 首先搜索每个线程的异常处理器,然后再搜索一个 ThreadGroup 的异常处理器。ThreadGroup 中的默认异常处理器实现将异常处理工作逐层委托给它的上层 ThreadGroup,直到其中某个 ThreadGroup 的异常处理器能够处理该未捕获异常,否则将一直传递到顶层的 ThreadGroup。顶层的 ThreadGroup 的异常处理器委托给默认的系统处理器(如果存在,在默认情况下为空),否则将把栈追踪信息输出到控制台。

下面我们来看一下 UncaughtExceptionHandler 接口:

public interface UncaughtExceptionHandler {
	void uncaughtException(Thread t, Throwable e);
}

异常处理器如何处理未捕获异常,取决于对服务质量的需求。最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序日志中。如下所示:

public class UEHLogger implements Thread.UncaughtExceptionHandler {
	public void uncaughtException(Thread t, Throwable e) {
		Logger logger = Logger.getAnonymousLogger();
		logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
	}
}

当然,异常处理器还可以采取更直接的响应,例如尝试重新启动线程,关闭应用程序,或者执行其他修复或诊断等操作。

在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。

实际上很多场景下,我们都是在使用线程池,那么该如何为线程池中的所有线程指定一个异常处理器呢?

要为线程池中的所有线程设置一个 UncaughtExceptionHandler,需要为 ThreadPoolExecutor 的构造函数提供一个 ThreadFactory

标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个 try-finally 代码块来接收通知,因此当线程结束时,将有新的线程来代替它。

如果没有提供捕获异常处理器或者其他的故障通知机制,那么任务会悄悄失败,从而引起更大的问题。

如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的 RunnableCallable 中,或者改写 ThreadPoolExecutorafterExecute 方法。

另外需要注意的是:

  • 只有通过 execute 提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过 submit 提交的任务,无论是抛出的 未检查异常 还是 已检查异常,都将被认为是任务返回状态的一部分。
  • 如果一个由 submit 提交的任务由于抛出了异常而结束,那么这个异常将被 Future.get 封装在 ExecutionException 中重新抛出。

2. JVM关闭

JVM 既可以 正常关闭,也可以 强行关闭

正常关闭的触发方法有多种,如下:

  • 当最后一个 “正常(非守护)” 线程结束时
  • 当调用了 System.exit
  • 通过其他特定于平台的方法关闭(例如发送了 SIGINT 信号或键入 Ctrl+C

强行关闭的触发方法,有如下:

  • 调用 Runtime.halt(int status)
  • 在操作系统中 “杀死” JVM 进程(例如发送 SIGKILL

说到 JVM 正常关闭,就不得不提接下来的主角 – 关闭钩子

2.1 关闭钩子

何为关闭钩子 ?

关闭钩子是指通过 Runtime.addShutdownHook 注册的但尚未开始的线程。它只有在 JVM 正常关闭才会执行,在强制关闭时不会执行。

JVM 关闭过程中,有哪些需要注意的呢 ?

  • 在正常关闭中,JVM 首先调用所有已注册的关闭钩子(Shutdown Hook)。不过 JVM 并不能保证关闭钩子的调用顺序。

  • 在关闭应用程序线程时,如果有(守护或非守护)线程仍然在运行,那么这些线程接下来将与关闭进程并发执行。

  • 当所有的关闭钩子都执行结束时,如果 runFinalizersOnExittrue,那么 JVM 将运行 终结器,然后再停止。

  • JVM 并不会停止或中断任何在关闭时仍然运行的应用程序线程。当 JVM 最终结束时,这些线程将被强行结束。

  • 如果 关闭钩子终结器 没有执行完成,那么正常关闭进程 “挂起” 并且 JVM 必须被强行关闭。当被强行关闭时,只是关闭 JVM,而不会运行关闭钩子。

关闭钩子在编写和使用上应该注意什么 ?

  • 关闭钩子应该是 线程安全 的。它们在访问共享数据时必须使用同步机制,并且小心地避免发生死锁,这与其他并发代码的要求相同。
  • 关闭钩子不应该对应用程序的状态(例如,其他服务是否已经关闭,或者所有的正常线程是否已经执行完成)或者 JVM 的关闭原因做出任何假设,因此在编写关闭钩子的代码时必须考虑周全。
  • 关闭钩子必须 尽快退出,因为它们会延迟 JVM 的结束时间,而用户可能希望 JVM 能尽快终止。

说了这么多,那关闭钩子可以用来做什么呢 ?

关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。

下面我们再来看一个示例【通过注册一个关闭钩子来停止日志服务】:

	public void start() {
		Runtime.getRuntime().addShutdownHook(new Thread(){
			public void run() {
				try {
					LogService.this.stop();
				} catch (InterruptedException ignored) {
					//
				}
			}
		}); 
	}

上述示例是 LogService 在其 start 方法中注册一个关闭钩子,从而确保在退出时关闭日志文件。

由于关闭钩子将并发执行,因此在关闭日志文件时可能导致其他需要日志服务的关闭钩子产生问题。那为了避免这种情况,关闭钩子不应该依赖那些可能被应用程序或其他关闭钩子关闭的服务。

实现上述功能的一种方式是对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之间出现竞态条件或死锁等问题。

无论是否使用关闭钩子,都可以使用这项技术,通过将各个关闭操作串行执行而不是并行执行,可以消除许多潜在的故障。

当应用程序需要维护多个服务之间的显式依赖信息时,上述可以确保关闭操作按照正确的顺序执行。

2.2 守护线程

何为守护线程?

线程可分为两种:普通线程守护线程。在 JVM 启动时创建的所有线程中,除了 主线程 以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。

什么情况下,我们需要使用守护线程 ?

有时候,我们希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍 JVM 的关闭。

讲到这里,那么在主线程中创建的线程,都是什么线程呢 ?

当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是 普通线程

普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。

当一个线程退出时,JVM 会检查其他正在运行的线程,如果这些线程都是守护线程,那么 JVM 会正常退出操作。当 JVM 停止时,所有仍然存在的守护线程都将被抛弃–即不会执行 finally 代码块,也不会执行回卷栈,而 JVM 只是直接退出。

需要注意的是:

  • 我们应当尽可能少地使用守护线程 — 很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含 I/O 操作的任务,那么这将是一种危险的行为。
  • 守护线程最好用于执行 “内部” 任务,例如周期性地从内存的缓存中移除逾期的数据。
  • 守护线程也不能用来替代应用程序管理程序中各个服务的生命周期。

2.3 终结器

当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他的一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式地交还给操作系统。

为了实现这个功能,垃圾回收器对那些定义了 finalize 方法的对象会进行特殊处理:在回收器释放它们后,调用它们的 finalize 方法,从而保证一些持久化的资源被释放。

由于终结器可以在某个由 JVM 管理的线程中运行,因此终结器访问的任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步。终结器并不能保证它们在何时运行甚至是否会运行,并且复杂的终结器通常还会在对象上产生巨大的性能开销。

大多数情况下,通过使用 finally 代码块和显式的 close 方法,能够比使用终结器更好地管理资源。唯一例外的情况在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。

最后需要注意: 我们应当尽量避免编写和使用包含终结器的类(除非是平台库中的类)
在这里插入图片描述

总结

本篇介绍了任务关闭剩下的内容【处理非正常的线程终止JVM 关闭】,那 《任务关闭》 的内容就告一段落了;下一篇博文,我们将开始正式介绍 《线程池的使用》,敬请期待!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java并发编程学习15-任务关闭(下) 的相关文章

随机推荐

  • 暗黑大陆游戏服务器为空 请检查列表文件,游戏服务器为空请检查列表文件名...

    游戏服务器为空请检查列表文件名 内容精选 换一换 当应用中的云硬盘空间不足时 可对该云硬盘进行扩容操作 扩容操作属于高危操作 请谨慎操作 为了防止数据丢失 磁盘只支持扩容 不支持缩容 如果磁盘已挂载在云服务器上 需要您通过云服务器控制台重启
  • ES多个字段聚合分组,在结果上执行二次统计分析

    Es版本 7 6 2 Test public void groupBucket 设置要查询的索引 SearchRequest request new SearchRequest indices EdaIndex EDaIndex FILE
  • JNI排坑记录:java.lang.UnsatisfiedLinkError导致JVM崩溃

    本次坑不大不小 在此记录一下 希望能够帮助遇到同样问题的朋友 1 背景 最近在进行Seetaface6开源人脸识别库的Java封装工作 封装工作初步完成后 Java端进行封装测试 调用库dll文件都成功了 但调用本地Native方法 dll
  • 基础编程题目集——7-15 计算圆周率

    1 题目要求 根据下面关系式 求圆周率的值 直到最后一项的值小于给定阈值 输入格式 输入在一行中给出小于1的阈值 输出格式 在一行中输出满足阈值条件的近似圆周率 输出到小数点后6位 2 样例 输入样例 0 01 输出样例 3 132157
  • QHash的使用(插入、取值、遍历、删除)

    1 创建 键值对的方式插入 数据类型随意 这里以键int 值QString示例 QHash
  • 200行代码写一个简易的C++小黑窗贪吃蛇游戏

    分享一个简易的小黑窗贪吃蛇 一共就两百行代码左右 包含注释 很适合初学者巩固语法来练练手 如果后续需要其他功能也可以再添加 先小小展示一下 源码在文末免费领取 使用工具 VS2019 不是用VS的也可以直接找出cpp和h文件复制到你们用的I
  • 在 RedHat 8.7 中 安装 ROCm

    1 official docs 1 1 graphics docs Overview amdgpu graphics and compute stack unknown build documentation 1 2 compute doc
  • 异步加载Baidu地图失败error isTrusted:true

    为什么会加载失败 为了用户体验 在用户使用地址组件之前不加载地图 因此我们采用官网的例子异步加载百度地图 这里做了一点改进 export function baiduMapInit const url https api map baidu
  • Ajax-爬取多页图片

    文章目录 一 目的 二 代码 三 结果 四 关键点 一 目的 爬取多页图片 图片网页地址 https picsum photos images 关键点 上述的网页HTML代码中并无图片地址 图片是通过Ajax请求的json数据 我们需要找到
  • Python创建虚拟环境

    目的 不同的程序需要不同的运行环境 方法 conda create n 虚拟环境名 python3 8 结果 在anaconda envs 目录下有一个新的目录 里面是一个全新的python 1 设置pip国内源 原因 python编程经常
  • KVM模块单独编译(适合调试)

    当前系统环境 CentOS Linux release 7 2 1511 Core 在说kvm模块单独编译之前 难免设计到linux内核模板的编写 所以这里也稍微提一下 1linux内核模块环境搭建 这里有2种方法 1 1 升级内核 升级当
  • 指令报错总结

    指令1 rosdep update 报错 error loading sources list The read operation timed out 解决博文 https blog csdn net weixin 44028876 ar
  • Flutter(一):Flutter环境的搭建

    前言 正好在研究flutter 把它系统化的整理一下
  • RBF神经网络和拟合实例

    RBF神经网络及拟合实例 RBF神经网络介绍 RBF神经网络结构 RBF神经网络算法 RBF神经网络逼近算法 采用RBF神经网络逼近非线性函数 神经网络逼近结果 代码如下 RBF神经网络介绍 RBF神经网络结构 径向基函数 Radial B
  • Task2:用T-SQL语句创建数据表

    Task2 用T SQL语句创建数据表 例1 使用T SQL语句操作创建用户信息表 表名为Users 任务要求 1 输入代码 2 全屏截图 分析题意 创建Users数据表的T SQL语句如下所示 use testdb 使用名为testdb的
  • Acwing-861. 二分图的最大匹配

    include
  • ChatGPT影响不可逆,与AI“共存”才是大趋势

    不久前 英国24所顶尖大学联合宣布要撤销ChatGPT禁令 不但联合宣布允许学生和教职员工在合乎道德的情况下使用生成式人工智能 还宣布学校会亲自指导学生使用 此消息一出来可是让英国留学生们炸开了锅 几个月前 二十多所英国学校联合宣布明确禁止
  • yii2-ueditor

    扩展下载 yii2 0 ueditor 框架下载 Yii 2 0 6 高级版 描述 最佳适用于yii2 0 高级版 advanced 应用框架 对于基础板 basic 及其他框架要修改对应的命名空间即可使用 效果演示 版本相关 Yii 2
  • Redis主从架构:主从同步和哨兵机制

    Redis主从架构 主从同步和哨兵机制 一 Redis主从架构 二 主从同步 2 1 引入 2 2 原理 1 全量同步 2 增量同步 3 优化Redis主从集群 2 3 总结 三 哨兵机制 3 1 引入 3 2 作用 3 3 原理 1 状态
  • Java并发编程学习15-任务关闭(下)

    任务关闭 下 任务关闭 由于篇幅较多 拆分了两篇来介绍各种任务和服务的关闭机制 以及如何编写任务和服务 使它们能够优雅地处理关闭 1 处理非正常的线程终止 我们知道 当单线程的控制台程序由于发生了一个未捕获的异常而终止时 程序将停止运行 并