血的教训--如何正确使用线程池submit和execute方法
血的教训之背景:使用线程池对存量数据进行迁移,但是总有一批数据迁移失败,无异常日志打印
凶案起因
听说parallelStream并行流是个好东西,由于日常开发stream串行流的场景比较多,这次需要写迁移程序刚好可以用得上,那还不赶紧拿来装*一下,此时不装更待何时。机智的我还知道在 JVM 的后台,使用通用的 fork/join 池来完成上述功能,该池是所有并行流共享的,默认情况,fork/join 池会为每个处理器分配一个线程,对应的变通方案就是创建自己的线程池如
ForkJoinPool pool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
pool.submit(() -> {
list.parallelStream().collect(Collectors.toList());
});
于是地雷就是从这里埋下的。
submit还是execute
public static void main(String[] args) throws InterruptedException, ExecutionException {
final ExecutorService pool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
List<Integer> list = Lists.newArrayList(1, 2, 3, null);
//1.使用submit
pool.submit(() -> {
list.parallelStream().map(a -> a.toString()).collect(Collectors.toList());
});
TimeUnit.SECONDS.sleep(3);
//2.使用 execute
pool.execute(() -> {
list.parallelStream().map(a -> a.toString()).collect(Collectors.toList());
});
//3.使用submit,调用get()
pool.submit(() -> {
list.parallelStream().map(a -> a.toString()).collect(Collectors.toList());
}).get();
TimeUnit.SECONDS.sleep(3);
}
读者自行跑一下上面的用例,会发现单独使用submit方法的并不会打印出错误日志,而使用execute方法打印出了错误日志,但是对submit返回的FutureJoinTask调用get()方法,又会抛出异常。于是真相大白,部分批次中的数据存在脏数据,为null值,遍历到该null值的时候出现了异常,但是异常日志在submit方法中给catch住,没有打印出来(心痛的感觉),而被捕获的异常,被包装在返回的结果类FutureJoinTask中,并没有再次抛出。
如果不需要异步返回结果,请不要用submit 方法
结论先行,我犯的错误就是,浅显的认为submit和execute的区别就只是一个有返回异步结果,一个没有返回一步结果,但是事实是残酷的。在submit()中逻辑一定包含了将异步任务抛出的异常捕获,而因为使用方法不当而导致该异常没有再次抛出。
现在提出一个问题,ForkJoinPool#submit()中返回的ForkJoinTask可以获取异步任务的结果,现这个异步抛出了异常,我们尝试获取该任务的结果会是如何? 我们直接看ForkJoinTask#get()的源码。
public final V get() throws InterruptedException, ExecutionException {
int s = (Thread.currentThread() instanceof ForkJoinWorkerThread) ?
doJoin() : externalInterruptibleAwaitDone();
Throwable ex;
if ((s &= DONE_MASK) == CANCELLED)
throw new CancellationException();
//这里可以直接看到,异步任务出现异常会在调用get()获取结果的时候,会被包装成ExecutionException再次抛出
if (s == EXCEPTIONAL && (ex = getThrowableException()) != null)
throw new ExecutionException(ex);
return getRawResult();
}
异步任务出现异常会在调用get()获取结果的时候,会被包装成ExecutionException再次抛出,但是异常是在哪里被捕获的呢?万变不离其宗,所有线程的线程都需要重写Thread#run()方法, 投递到ForkJoinPool的线程会被包装成ForkJoinWorkerThread,因此我们看一下ForkJoinWorkerThread#run()的实现.
public void run() {
if (workQueue.array == null) { // only run once
Throwable exception = null;
try {
onStart();
pool.runWorker(workQueue);
} catch (Throwable ex) {
//出现异常,捕获,再次抛出会在调用ForkJoinTask#get()的时候
exception = ex;
} finally {
try {
onTermination(exception);
} catch (Throwable ex) {
if (exception == null)
exception = ex;
} finally {
pool.deregisterWorker(this, exception);
}
}
}
}
上面的分析是基于ForkJoinPool的,是不是所有的线程池的submit和execute方法的实现都是类似这样,我们常用的线程池ThreadPoolThread实现会是怎样的,同样的思路,我们需要找到投递到ThreadPoolThread的异步任务最终被包装为哪个Thread的子类或者是实现java.lang.Runnable#run,答案就是java.util.concurrent.FutureTask
public void run() {
...
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
//捕获异常
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
}
....
}
总结
java.util.concurrent.ExecutorService#submit(java.lang.Runnable)为何线程池会有这种设定,实际上我们的思路不应该局限于线程池,而是放在获取异步任务结果,异常是否也是属于异步结果,FutureTask作为JDK提供的并发工具类的实现中,已经给出了很好的答案,即获取异步任务结果,异常也是属于异步结果,如果异步任务出现运行时异常,那么在获取该任务的结果时,该异常会被重新包装抛出
作者:plz叫我红领巾
出处:https://juejin.im/post/5d15c430f265da1bab29c1fe
本博客欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。码子不易,您的点赞是我习作最大的动力
血的教训--如何正确使用线程池submit和execute方法的更多相关文章
- 12.ThreadPoolExecutor线程池原理及其execute方法
jdk1.7.0_79 对于线程池大部分人可能会用,也知道为什么用.无非就是任务需要异步执行,再者就是线程需要统一管理起来.对于从线程池中获取线程,大部分人可能只知道,我现在需要一个线程来执行一个任 ...
- Java 线程池submit和execute
submit方法: public abstract class AbstractExecutorService implements ExecutorService { protected <T ...
- 线程池的submit和execute方法区别
线程池中的execute方法大家都不陌生,即开启线程执行池中的任务.还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果.下面简要介绍一下两者的三个 ...
- 线程池中 submit()和 execute()方法有什么区别?(未完成)
线程池中 submit()和 execute()方法有什么区别?(未完成)
- 线程池续:你必须要知道的线程池submit()实现原理之FutureTask!
前言 上一篇内容写了Java中线程池的实现原理及源码分析,说好的是实实在在的大满足,想通过一篇文章让大家对线程池有个透彻的了解,但是文章写完总觉得还缺点什么? 上篇文章只提到线程提交的execute( ...
- Java线程池中submit() 和 execute()方法的区别
两个方法都可以向线程池提交任务, execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorS ...
- Java线程池ThreadPoolExecuter:execute()原理
一.线程池执行任务的流程 如果线程池工作线程数<corePoolSize,创建新线程执行task,并不断轮训t等待队列处理task. 如果线程池工作线程数>=corePoolSize并且等 ...
- 线程池系列一:线程池作用及Executors方法讲解
线程池的作用: 线程池作用就是限制系统中执行线程的数量. 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果:少了浪费了系统资源,多了造成系统拥挤效率不高.用线程池控制线程数量 ...
- 线程池ThreadPoolExecutor的使用方法
方法我们通过继承Thread类和实现runnable接口或者callable接口三种方式实现. 继承Thread类实际上也是实现了runnable接口,被继承的类主要是实现run()方法,通过star ...
随机推荐
- Step-By-Step Installation of RAC with RAW Datafiles on Windows 2000
Step-By-Step Installation of RAC with RAW Datafiles on Windows 2000 Purpose This document will pr ...
- 简明Python3教程 18.下一步是什么
如果你有认真通读本书之前的内容并且实践其中包含的大量例程,那么你现在一定可以熟练使用python了. 同时你可能也编写了一些程序用于验证python特性并提高你的python技能.如果还没有这样做的话 ...
- 使用WPF创建画图箭头
原文:使用WPF创建画图箭头 今天要给leader line画个箭头,所以就google一下,找到下面的文章,写的不错,可以实现我的需求,所以就摘录下来. 我把源代码中的arraw.cs加入到我的工程 ...
- Linux栈搜索算法优化随想
Linux网络协议栈可以准确但仍进行说明,不用说,Netfilter.简单地说,TC够了,但有几个硬伤,本文不构成一个完整的记录,如果是随笔,不可当真. 0.发现物种 Linux堆栈作为一个纯软件实现 ...
- AntDesign Form表单字段校验的三种方式
1.使用getFieldDecorator的rules规则 最简单的方法就是使用getFieldDecorator中的rules验证.rules中定义校验规则,message为校验不通过时的提示文字. ...
- zlog 程序日志的库 交叉编译(Linux生成ARM库,观察执行步骤)
1. zlog 是个很好的写程序日志的库,功能比较强大,上手快. 2. 下载地址:https://github.com/bmanojlovic/zlog 3. cd 到文件夹下,对 autogen.s ...
- html5 命运之轮生产
码,如以下: <%@ page language="java" contentType="text/html; charset=UTF-8" pageEn ...
- Go 语言如果按这样改进,能不能火过 Java?
据 InfoWorld 消息,为改进 Go 语言的开发工具,Go 可能会获得自己的语言服务器,类似于 Microsoft 和 Red Hat 的语言服务器协议. 消息是从 Go 语言开发者的讨论组中流 ...
- Bootstrap 图片形状
@{ Layout = null;}<!DOCTYPE html><html><head> <meta name="viewport&q ...
- XML Serialize/Deserialize
using System; using System.Collections.Generic; using System.Globalization; using System.IO; using S ...