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

news2024/12/27 13:33:28

任务关闭(下)

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

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.SERVER, "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 关闭】,那 《任务关闭》 的内容就告一段落了;下一篇博文,我们将开始正式介绍 《线程池的使用》,敬请期待!

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

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

相关文章

Urban NeRF

本文首发于馆主君晓的博客,文章链接 简要介绍 这是谷歌和多伦多大学合作的一篇发表在CVPR2022上的工作,延续NeRF重建的相关思路。考虑到之前的一些工作要么是在合成数据集上进行的NeRF重建,要么就是用到真实的场景,但是场景很小&a…

JDK1.8和JDK1.7的HashMap源码分析以及线程不安全问题

参考: 教你如何阅读HashMap源码~吊打面试官 - 腾讯云开发者社区-腾讯云 (tencent.com) 有一些面试题 Map - HashSet & HashMap 源码解析 | Java 全栈知识体系 (pdai.tech) HashMap源码&底层数据结构分析 | JavaGuide(Java面试学习指南) hashmap头插法和尾插…

LAB1 VRRP实验

■实验拓扑 ■实验需求 多厂商的网关冗余(VRPP) 考虑上行/上上行/下行链路的之间的track 生成树配置 VPC能访问R4的loopback口地址(8.8.8.8) ■实验步骤 ▶思科路由器CISCO-R4 Router(config)#hostname CISCO-R4 CISCO-…

【博客581】为什么MASQUERADE都在POSTROUTING做

为什么MASQUERADE都在POSTROUTING做 MASQUERADE都在POSTROUTING做,为什么不能在output做 1、iptables flow graph: 2、output之后的routing和rerouting: 对于本机 app 发出(outcoming)的流量,netfilter 有2次 routing 过程&…

算法刷题打卡第59天:相交链表

相交链表 难度:简单 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始相交: 题目数据 保证 整个链式结构中不存在环。 …

空洞卷积atrous/dilated convolution

1、定义 空洞卷积(atrous/dilated convolution)又称膨胀卷积是针对图像语义分割问题中下采样会降低图像分辨率、丢失信息而提出的一种卷积思路。空洞卷积向卷积层引入了一个称为“扩张率/膨胀率(dilation rate)”的新参数,该参数定义了卷积核…

Excel 个人财务:如何在 Excel 模板中创建预算

wpcmf “金钱是一种工具。使用得当,它会变得美丽——使用不当,它会变得一团糟!” – 布拉德利文森 正确使用金钱需要纪律。在本教程中,我们将了解如何使用 Excel 进行个人财务以正确管理我们的预算和财务。我们将涵盖以下主题。 …

Java使用spire进行word文档的替换

前言 今天遇到一个需求,需要对word模板进行替换制定的变量 在网上找了很多方案,做了很多的demo,下面就把我觉得比较简单的一种分享给大家 本次的主角是:spire.doc spire.doc是专门实现对word的操作(包括文字&#…

「数据密集型系统搭建」原理篇|OLAP、OLTP,竟是两个世界

本篇来聊聊OLAP与OLTP的区别以及它们各自的适用场景,以此话题为导引和大家聊聊技术视野与知识储备对于研发同学的重要性,最后站在事务处理与在线分析的角度分别论述下两个数据世界的底层构建逻辑。 OLAP、OLTP的概念与区别 概念 了解OLAP、OLTP的概念&…

【CANN训练营第三季】学习ascend-CANN遇到的经典疑难问题总结

1、/home/HwHiAiUser/samples_1/cplusplus/level2_simple_inference/1_classification/resnet50_imagenet_classification/src/…/inc/utils.h:13:10: fatal error: acl/acl.h: No such file or directory #include “acl/acl.h” 原因:放错了DDK——PATH export D…

【Unity3D】快速上手 EasyAR

目录 一,AR技术 1.AR简介 2.AR特点 3.AR工作原理 二,EasyAR 插件 1.获取Key 2.EasyAR 插件下载和导入 三,快速上手 EasyAR 废话不多说上运行效果 一,AR技术 1.AR简介 AR(Augmented Reality,增强现…

RedLock算法(红锁算法)介绍

文章目录一. 部署图二. RedLock算法简单介绍加锁解锁一. 部署图 各redis独立部署,各自独立 二. RedLock算法简单介绍 加锁 应用程序获取系统当前时间应用程序使用相同的kv值依次从多个redis实例中获取锁。 如果某一个节点超过一定时间依然没有获取到锁则直接放…

Porjet1 小白学习CANoe16安装、新建工程、新建数据库、简单运行

准备工作 1,下载CANoe16(因为笔者只找到了官方提供的CANoe16的DEMO license) 2,安装CANoe16,点击默认安装即可,不需要安装驱动。 3,如果桌面没有找到CANoe16的打开方式可以参考 解决安装CANoe1…

OpenGL之Shader编程入门

1.shader 编程基础 1.1 Vertex shader与Fragment shader Vertex shader即顶点着色器,用来改变顶点的属性。Fragment shader即片元着色器,用来改变片元的颜色,在Direct3D中称为Pixel shader,像素着色器。 1.2 编程语言 面向OpenG…

C语言快速互转HEX(16进制)和原始字符串/数组

C语言快速互转HEX(16进制)和原始字符串/数组缘由这个起因是昨晚群里有人在讨论怎么把字符串转成HEX方法最佳,讨论到最后变成哪种方法效率最优了。毕竟这代码是要在MCU上面跑的,要同时考虑到时间和空间的最优解。当然讨论的是有结果…

Java8流式计算相关

目录 lambda 优点 语法介绍 语法格式一 : 语法格式二 : 语法格式三 : 语法格式四 : 语法格式五 : 语法格式六 : 方法引用 stream Stream流的常用方法: 创建动态list 创建固定长度list map filter groupingBy sum list转map: map转li…

谷粒商城学习笔记

docker 安装docker docker官方centos镜像下载地址:https://docs.docker.com/engine/install/centos/ 步骤: 先卸载,如果不是root用户在前边加上sudo sudo yum remove docker \docker-client \docker-client-latest \docker-common \docke…

C 程序设计教程(05)—— C 语言的数据类型(三):指针类型

C 程序设计教程(05)—— C 语言的数据类型(三):指针类型 该专栏主要介绍 C 语言的基本语法,作为《程序设计语言》课程的课件与参考资料,用于《程序设计语言》课程的教学,供入门级用…

MySql中json类型数据的查询以及在MyBatis-Plus中的使用

表结构和初始数据 新建表结构 CREATE TABLE json_test (id int NOT NULL AUTO_INCREMENT,roles json DEFAULT NULL COMMENT 角色,project json DEFAULT NULL COMMENT 项目,PRIMARY KEY (id) ) ENGINEInnoDB;初始数据 INSERT INTO ctts_dev.json_test(id, roles, project) VALU…

SpringBoot 整合 xxl-job

文章目录部署 xxl-jobSpringBoot 配置maven 配置application.yaml配置 XxlJobConfigXxlJobSpringExecutor新建执行任务配置 xxl-job-admin执行器管理任务管理部署 xxl-job K8S 部署 xxl-job 参考文档:https://blog.csdn.net/weixin_42555971/article/details/12489…