ScheduledThreadPoolExecutor 吞异常
线程挂掉不一定会输出日志到控制台,比如ScheduledThreadPoolExecutor,如果在执行的任务中有未捕获的异常抛出,就行停止调度,没有任何错误输出到控制台或日志文件。在项目中这会导致一些非常奇怪的错误,并且常难以发现。
当怀疑线程挂掉时可以在run方法加try catch,打印错误日志。当然,更好的习惯是每次使用ScheduledThreadPoolExecutor线程池都在run()方法里加上try-catch。
解决线上问题,日志真的很重要!!!
另转载一篇文章,写得很好,原文地址:https://my.oschina.net/lifany/blog/884002
一、前言
线程池技术是服务器端开发中常用的技术。不论是直接还是间接,各种服务器端功能的执行总是离不开线程池的调度。关于线程池的各种文章,多数是关注任务的创建和执行方面,对于异常处理和任务取消(包括线程池关闭)关注的偏少。
接下来,本文将从 Java 原生线程、两种主要线程池 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
这三方面介绍 Java 中线程的异常处理机制。
二、Thread
在谈线程池的异常处理之前,我们先来看 Java 中线程中的异常是如何被处理的。大家都知道如何创建一个线程任务:
代码1
Thread t = new Thread(() -> System.out.println("Execute in a thread"));
t.start();
为了简化代码,这里使用了 Java 8 的 Lambda 表达式。() -> System.out.println("Execute in a thread")
等同于在 Runnable
中执行 System.out.println
方法。后面不再解释。
如果这个任务抛出了异常,那又会怎样:
代码2
Thread t = new Thread(() -> System.out.println(1 / 0));
t.start();
如果我们执行上面这段代码,会在控制台上看到异常输出。可能多数同学会对此不会觉得问题,但是问题在于,通常情况下绝大多数线上应用不会将控制台作为日志输出地址,而是另有日志输出。这种情况下,上面的代码所抛出异常便会丢失。
那为了将异常输出到日志中,我们会这样写代码:
代码3
Thread t = new Thread(() -> {
try {
System.out.println(1 / 0);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
});
t.start();
这样我们就能异常栈输出到日志中,而不是控制台,从而避免异常的丢失。
过了一段时间,问题又来了,可能好多线程任务默认的异常处理机制都是相同的。比如都是将异常输出到日志文件。按照上面的写法会造成重复代码。虽然重复的不多,但是有代码洁癖的小伙伴可能也会觉得不舒服。
那我们该如何解决这个问题呢?其实 JDK 已经为我们想到了,Thread
类中有个接口 UncaughtExceptionHandler
。通过实现这个接口,并调用 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler)
方法,我们就能为一个线程设置默认的异常处理机制,避免重复的 try...catch
了。
除此以外,我们还可以通过 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler)
设置全局的默认异常处理机制。此外,ThreadGroup
也实现了 UncaughtExceptionHandler
接口,所以通过 ThreadGroup
还可以为一组线程设置默认的异常处理机制。
其实,之所以代码2在执行之后我们能在控制台上看到异常,也是因为 UncaughtExceptionHandler
机制。ThreadGroup
默认提供了异常处理机制如下:
代码4
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
// 最终执行如下代码
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
三、ThreadPoolExecutor
在 Java 5 发布之后,线程池便开始越来越广泛地用于创建并发任务。多数时候,当说到 Java 的线程池时,我们一般指的就是 ThreadPoolExecutor
。那在 ThreadPoolExecutor
中是如何处理异常的呢?
代码5
Executors.newSingleThreadExecutor().execute(() -> {
throw new RuntimeException("My runtime exception");
});
上面的代码的异常处理机制其实同直接使用 Thread
是一样的。所以也有同样的问题,异常信息无法反映在日志文件中。解决这个问题的方法同上一节一样:在每个 Runnable
中编写 try ... catch
语句;或者使用 UncaughtExceptionHandler
机制。
我们先来看如何为线程池中的工作线程设置 UncaughtExceptionHandler
。
为线程池工作线程设置 UncaughtExceptionHandler
简单来说,就是通过 ThreadFactory
。通过 ThreadPoolExecutor
的构造函数和 Executors
中的工具方法,我们都可以为新创建的线程池设置 ThreadFactory
。
ThreadFactory
是个接口,它只定义了一个方法 Thread newThread(Runnable r)
。在这个方法中,我们可以为新创建出来的线程设置 UncaughtExceptionHandler
。当然,这样写起来显得很麻烦,好在 Apache Commons 和 Google Guava 这两个最有名的 Java 工具类库都为我们提供了相应的类库以简化配置 ThreadFactory
的工作。下面以 Apache Commons 提供的 BasicThreadFactoryBuilder
为例
代码6
ThreadFactory executorThreadFactory = new BasicThreadFactory.Builder()
.namingPattern("task-scanner-executor-%d")
.uncaughtExceptionHandler(new LogUncaughtExceptionHandler(LOGGER))
.build();
Executors.newSingleThreadExecutor(executorThreadFactory);
UncaughtExceptionHandler 一定起作用吗?
此话怎讲呢?其实 ThreadPoolExecutor
为执行并发任务提供了两种方法:execute(Runnable)
和 submit(Callable/Runnable)
。之前的代码示例只演示了执行 execute(Runnable)
时的情况。那在设置了默认的 UncaughtExceptionHandler
之后,当执行 submit(Callable/Runnable)
方法,抛出抛异常之后有会如何?
看下面的代码
代码7
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(new LogExceptionHandler())
.build();
Executors.newSingleThreadExecutor(threadFactory)
.submit(() -> {
throw new RuntimeException("test");
});
上面的程序执行完之后,不会在控制台或日志中看到任何输出,虽然设置了 UncaughtExceptionHandler
。要弄清原因,就要看一下 ThreadPoolExecutor
的源代码
代码8
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit
方法是调用 execute
实现任务执行的。但是在调用 execute
之前,任务会被封装进 FutureTask
类中,然后最终工作线程执行的是 FutureTask
中的 run
方法。
代码9:FutureTask.run
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
由上面的代码可以看出,不同于直接调用 execute
方法,调用 submit
方法后,如果任务抛出异常,会被 setException
方法赋给代表执行结果的 outcome
变量,而不会继续抛出。因此,UncaughtExceptionHandler
也没有机会处理。
如果想知道 submit
的执行结果是成功还是失败,必须调用 Future.get()
方法。
UncaughtExceptionHandler 是否适合在线程池中使用
从上面的分析中可以看出,使用 UncaughtExceptionHandler
,可以处理到使用 execute
方法执行任务所抛出的异常,但是对 submit
方法无效。那如果只是用 execute
方法,我们是否可以通过设置 UncaughtExceptionHandler
从而添加一种默认的异常处理机制,以避免重复的 try...catch
代码呢?
答案是不能。原因在于,如果在执行 execute
方法时不在 Runnable.run
方法中写 try...catch
方法,自然异常会交由 UncaughtExceptionHandler
处理,但是,在这之前,线程的工作线程会因为异常而退出。虽然线程池会创建一个新的工作线程,但是如果这个步骤反复执行,效率自然会下降很多。
四、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
是另一种常用的线程池,常用了执行延迟任务或定时任务。常用的方法为 scheduleXXX
系列。那在这个线程池中异常是如何处理的呢?
其实,如果看过前面的部分,到这里也基本能猜出来了。ScheduledThreadPoolExecutor
用来封装任务的是 ScheduledFutureTask
。ScheduledFutureTask
是 FutureTask
的子类,所以,异常也会被复制给 outcome
。
但是,这里还是有一些差异的。在使用 ThreadPoolExecutor.submit
和 ScheduledThreadPoolExecutor.schedule
方法时,我们可以通过这两个方法返回的 Future
来获得执行结果,这包括正常结果,也包括异常结果。但是,对于 ScheduledThreadPoolExecutor.scheduleWithFixedDelay
和 scheduleAtFixedRate
这两个方法,其返回的 Future
只会用来取消任务,而不是得到结果。原因也很容易理解,因为这两个方法执行的是定时任务,是反复执行的。这也是为什么这两个方法的任务定义使用了 Runnable
接口,而不是有返回值的 Callable
接口。因此,对于这两个方法来说,在 Runnable.run
方法中加 try...catch
是必须的,否则很有可能出错了却毫不知情。
五、结论
在 Thread
中,我们可以通过 UncaughtExceptionHandler
来实现默认的异常处理机制。但是在使用 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
这两个 JDK 最主要的线程池时,使用 UncaughtExceptionHandler
是不合适的。所以,try...catch
往往是不可避免的,否则你的任务很有可能失败的悄无声息。
ScheduledThreadPoolExecutor 吞异常的更多相关文章
- 【C#进阶系列】20 异常和状态管理
异常就是指成员没有完成它的名称所宣示的行动. public class Girl { public string Name { get; set; } } public class Troy{ Gir ...
- 有关于异常捕获点滴,plus我也揭揭java的短
▄︻┻┳═一『异常捕获系列』Agenda: ▄︻┻┳═一有关于异常捕获点滴,plus我也揭揭java的短 ▄︻┻┳═一根据异常自定义处理逻辑([附]java异常处理规范) ▄︻┻┳═一利用自定义异常来 ...
- Atitit.异常的设计原理与 策略处理 java 最佳实践 p93
Atitit.异常的设计原理与 策略处理 java 最佳实践 p93 1 异常方面的使用准则,答案是:: 2 1.1 普通项目优先使用异常取代返回值,如果开发类库方面的项目,最好异常机制与返回值都提供 ...
- 《Java学习笔记(第8版)》学习指导
<Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...
- 《java JDK7 学习笔记》之异常处理
1.java中所有的错误都会被打包为对象,JVM会尝试执行try区块中的程序代码,如果发生错误,执行流程会跳离错误发生点,然后比较catch括号中声明的异常类型,是否符合被抛出的错误对象类型,如果是的 ...
- 20145206邹京儒《Java程序设计》第5周学习总结
20145206 <Java程序设计>第5周学习总结 教材学习内容总结 第八章 8.1 语法与继承架构 package CH5; /** * Created by Administrato ...
- 201453408刘昊阳 《Java程序设计》第5周学习总结
201453408刘昊阳 <Java程序设计>第5周学习总结 教材学习内容总结 第8章 异常处理 8.1 语法与继承结构 8.1.1 使用try.catch p227代码(Average) ...
- 20145330第五周《Java学习笔记》
20145330第五周<Java学习笔记> 这一周又是紧张的一周. 语法与继承架构 Java中所有错误都会打包为对象可以尝试try.catch代表错误的对象后做一些处理. 使用try.ca ...
- 20145320 《Java程序设计》第5周学习总结
20145320 <Java程序设计>第5周学习总结 教材学习内容总结 8.1 语法与继承架构 try.catch Java中的错误会被包装为对象,而使用try与catch,JVM会执行t ...
随机推荐
- C#发邮件_EmailHelper
EmailHelper类 public class EmailHelper { /// <summary> /// 发送邮件 /// </summary> /// <pa ...
- Spring Boot2.0 整合 Kafka
Kafka 概述 Apache Kafka 是一个分布式流处理平台,用于构建实时的数据管道和流式的应用.它可以让你发布和订阅流式的记录,可以储存流式的记录,并且有较好的容错性,可以在流式记录产生时就进 ...
- Kubernetes学习之路(26)之kubeasz+ansible部署集群
目录 1.环境说明 2.准备工作 3.分步骤安装 3.1.创建证书和安装准备 3.2.安装etcd集群 3.3.安装docker 3.4.安装master节点 3.5.安装node节点 3.6.部署集 ...
- JS prototype 生成机制
默认的 prototype 属性是 Object() 对象,只不过每种类型或者自定义类型锁挂载的对象属性不同. 事实上,prototype 的生成是这样的: const Func = function ...
- .Net Core 在 Linux-Centos上的部署实战教程(四) ---- 总结
问题: 1.网站部署上访问不了,可能是防火墙/安全组的原因 2.在后台运行这块上 我查了一些类似的部署博客 好多人都是用守护进程搞的,本人也算Linux小白 不懂这样做的好处是啥 有大佬的话 可 ...
- vue中使用sass
1.npm安装 npm install sass-loader --save-dev npm install node-sass --save-dev //--save写入到package.json里 ...
- js中布尔值为false的六种情况
下面6种值转化为布尔值时为false,其他转化都为true 1.undefined(未定义,找不到值时出现) 2.null(代表空值) 3.false(布尔值的false,字符串"false ...
- Linux查看端口
1.lsof -i:端口号 用于查看某一端口的占用情况,比如查看8000端口使用情况,lsof -i:8000 2.netstat -tunlp |grep 端口号 用于查看指定的端口号的进程情况 ...
- HNOI2013 BZOJ3144 切糕
在n×m的表格上,在(x,y)填v的代价是w(x,y,v),且相邻格子填的数相差≤d.求填满表格的最小代价.n,m,maxv≤40. 每个点上选择一个数填,因此将上面的数串起来.考虑限制条件,矛盾条件 ...
- poj 1486 纸张与数字匹配(二分图+割边处理)
题目来源:http://poj.org/problem?id=1486 题意: 算出所有独一无二的字母与数字的组合,使二分图完全匹配 我以为所有点都要独一无二匹配时输出匹配方法 题解: 先得到一个完全 ...