血的教训之背景:使用线程池对存量数据进行迁移,但是总有一批数据迁移失败,无异常日志打印

凶案起因

​ 听说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.使用submitpool.submit(() -> {list.parallelStream().map(a -> a.toString()).collect(Collectors.toList());});TimeUnit.SECONDS.sleep(3);//2.使用 executepool.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 方法

​ 结论先行,我犯的错误就是,浅显的认为submitexecute的区别就只是一个有返回异步结果,一个没有返回一步结果,但是事实是残酷的。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 onceThrowable 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的,是不是所有的线程池的submitexecute方法的实现都是类似这样,我们常用的线程池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叫我红领巾

​ 出处:juejin.im/post/5d15c4…

本博客欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。码字不易,您的点赞是我写作的最大动力

转载于:https://juejin.im/post/5d15c430f265da1bab29c1fe

血的教训--如何正确使用线程池submit和execute方法相关推荐

  1. 一心多用多线程-细谈java线程池submit与execute的区别

    深夜学习,发现ThreadPoolExecutor里面一个小知识点,故开热点连wifi怒写submit与execute方法的区别. 1.问题的来源 在看书的时候,涉及到java线程池问题的时候常常面临 ...

  2. Java 线程池submit和execute

    submit方法: public abstract class AbstractExecutorService implements ExecutorService {protected <T& ...

  3. 线程池的submit和execute方法区别

    线程池中的execute方法大家都不陌生,即开启线程执行池中的任务.还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果.下面简要介绍一下两者的三个 ...

  4. java 线程池 execute_Java线程池的submit和execute方法区别

    线程池中的execute方法大家都不陌生,即开启线程执行池中的任务. 还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果. 下面简要介绍一下两者的 ...

  5. 正确关闭线程池:shutdown 和 shutdownNow 的区别

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/xiewenfeng520/ article/details/107013342 前言 本章分为两个议题 如何正确关闭线程池 s ...

  6. Java多线程学习九:如何正确关闭线程池?shutdown 和 shutdownNow 的区别

    如何正确关闭线程池?以及 shutdown() 与 shutdownNow() 方法的区别?首先,我们创建一个线程数固定为 10 的线程池,并且往线程池中提交 100 个任务,如代码所示. 复制代码 ...

  7. JAVA中创建线程池的五种方法及比较

    之前写过JAVA中创建线程的三种方法及比较.这次来说说线程池. JAVA中创建线程池主要有两类方法,一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用.另一类是通过Thr ...

  8. Java线程池中submit()和execute()方法有什么区别

    两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中,而submit()方法返回有计算结构的Future对象,它定义在ExecutorServic ...

  9. 【EventBus】EventBus 源码解析 ( 事件发送 | 线程池中执行订阅方法 )

    文章目录 一.EventBus 中主线程支持类 二.EventBus 中 AsyncPoster 分析 三.AsyncPoster 线程池 Runnable 任务类 一.EventBus 中主线程支持 ...

最新文章

  1. 微软无解!Win10用户突然减少:装回Win7
  2. 【浅墨Unity3D Shader编程】之三 光之城堡篇:子着色器、通道与标签的写法 amp; 纹理混合...
  3. Dataset之Facades:Facades数据集的简介、安装、使用方法之详细攻略
  4. java param request_使用@RequestParam将请求参数绑定至方法参数
  5. lodash源码分析之compact中的遍历
  6. mysql事务嵌套 php_使用以下代码,MySQL中的PHP“嵌套”事务是否...
  7. Nginx反向代理、动静分离、负载均衡及rewrite隐藏路径详解(Nginx Apache MySQL Redis)–第二部分...
  8. nginx ---- 配置成系统服务
  9. Flutter 深度学习 — Cookbook(Flutter菜谱)
  10. 分布式存储系统学习笔记(二)—分布式文件系统(1)—Google文件系统GFS
  11. svn在linux下的使用(svn命令行)删除 新增 添加 提交 状态查询 恢...
  12. redis读中文 | fastjson 的 map、string、json 三者互转
  13. 正在学习C++的屑人麻了
  14. RD、RT以及VRF是什么?
  15. html输入框字体字号设置,HTML网页怎么设置文本框里面输入的文字大小
  16. Mahout(机器学习引擎)
  17. ci/cd自动化测试_自动化CI / CD并花费更多时间编写代码
  18. 一个简单入门的Py笔记
  19. TeX Live + TeXstudio macOS 下载及安装
  20. Burp Suite 是用于攻击web 应用程序的集成平台

热门文章

  1. 综述 | 深度学习弱目标检测定位
  2. 李飞飞团队发布:中国AI期刊影响力首超美国
  3. 人工智能相关领域的国际顶尖会议介绍
  4. pytorch入门-简介及安装
  5. 从学习 Paddle 开始学习深度学习
  6. python安装opencv2.4.9_Python学习之一:Python2.7与opencv2.4安装配置
  7. Swagger2 使用说明
  8. Eclipse 皮肤
  9. 【GPU加速系列】PyCUDA(一):上手简单操作
  10. 编译安装php7.3