线程池submit和execute,搞不好会引发线上故障
写在前面
这两个方法都可以用来提交任务给线程池,但是又有所区别。我们先来看下二者的使用示例,先有个直观认识。
execute的使用示例:
public class ExecuteTest {
private final static ThreadPoolExecutor executor = new ThreadPoolExecutor(0,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
while (true) {
executor.execute(() -> {
System.out.println(atomicInteger.getAndAdd(1));
});
}
}
}
submit使用示例:
public class SubmitTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable = new Callable<String>() {
public String call() throws Exception {
System.out.println("This is submit method.");
return "submit is done";
}
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(callable);
System.out.println(future.get());
}
}
从上面的示例可以看出二者的区别:
execute
的入参是Runnable, 没有返回值。任务通过execute
提交后就基本和主线程脱离关系了。
而submit
的入参可以是Callable(也可以是Runnable),并且有返回值,返回的是一个Future
对象,然后通过对象的get
方法获取任务执行的结果。
平时使用的时候根据不同的业务场景决定使用哪个方法。如果使用不当的话可能会引起很严重的问题,下面就带你看几个例子。
任务统计导致OOM问题
下面这个例子是统计任务执行的次数,
/**
* 统计任务已经执行的数量
* key:任务名称
* value:数量
*/
private static Map<String, AtomicInteger> countTasks = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()){
/**
* 任务执行完后统计任务执行的数量
* @param r
* @param t
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
countTasks.compute(r.toString(), (s, atomicInteger) ->
new AtomicInteger(atomicInteger == null ? 0 : atomicInteger.incrementAndGet()));
}
};
/**
* 源源不断的任务添加进线程池被执行
*/
for (int i =0; i < 100; i++) {
threadPoolExecutor.execute(new SimpleRunnable());
}
CountDownLatch cd = new CountDownLatch(1);
cd.await(10, TimeUnit.SECONDS);
System.out.println(JSON.toJSONString(countTasks));
threadPoolExecutor.shutdownNow();
}
static class SimpleRunnable implements Runnable{
@Override
public void run() {
System.out.println("simple task");
}
@Override
public String toString(){
return this.getClass().getSimpleName();
}
}
执行的结果是:
{"SimpleRunnable":99}
这个是正常的,我们有一个任务,提交了99次到线程池,任务被执行了99次。
如果把execute
改成submit
,结果是怎样呢?如下:
{"java.util.concurrent.FutureTask@76f2781a":0,"java.util.concurrent.FutureTask@74bbc5ae":0,"java.util.concurrent.FutureTask@e34a8ea":0,"java.util.concurrent.FutureTask@2fd1ff2":0,"java.util.concurrent.FutureTask@2ec135dd":0,"java.util.concurrent.FutureTask@51e8edb8":0
....
我省略了打印结果其余的部分,不过你应该已经看出来了,改成submit
后,每次提交任务都是一个新的任务名,也就是SimpleRunnable
的toString
方法每次返回的值都不同。这个在线上的话我们的map很容易就被撑爆了。
为什么会出现这种情况呢?我们看下submit
里面都干了啥,
如上图所示,线程池通过submit方式提交任务,会把Runnable封装成FutrueTask
,每次提交都会创建一个新的实例。我们debug一下,把断点设置在afterExecute
方法里,如下图,r参数确实是FutrueTask
实例。
接着看,还有坑
异常无法抛出的问题
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
List<Integer> list = Lists.newArrayList(1, 2, 3, null);
threadPoolExecutor.submit(() -> {
List<String> result = list.stream().map(a -> a.toString()).collect(Collectors.toList());
System.out.println(JSON.toJSONString(result));
});
CountDownLatch cd = new CountDownLatch(1);
cd.await(2, TimeUnit.SECONDS);
threadPoolExecutor.shutdownNow();
}
list里面包含一个null元素,很显然在执行list.stream().map
时会报错,但是你运行下看看会发现什么也没发生。(不着急,等下再说原因),然后改成execute
试试,运行会报如下的错误:
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.app.GCTest.lambda$null$0(GCTest.java:28)
at com.app.GCTest$$Lambda$2/215004560.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
at com.app.GCTest.lambda$main$1(GCTest.java:28)
at com.app.GCTest$$Lambda$1/636718812.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:744)
嗯,这个跟我们预期的一样。
这可是个大坑啊,必须要深挖一下原因。顺着submit一层层进入源码中,最终Runnable
接口的run
方法,前面说了submit提交的任务会被包装成FutureTask
实例,所有最终是调用它的run
方法,源码如下:
很明显,异常在内部被“吃掉了”。不过从源码可以看到异常被set出来了,应该是可以用get主动拉取到的。我们可以来做个测试。
在上面的代码示例中加入如下:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()){
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
t.printStackTrace();
} else {
if (r instanceof Future<?>) {
try {
//get这里会首先检查任务的状态,然后将上面的异常包装成ExecutionException
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
t.printStackTrace();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
}
}
};
然后再运行,发现异常信息可以正常打印了。
所以,如果使用submit,要么用get拉取异常处理, 要么自己写try catch把任务执行的逻辑包起来。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Stax解析XML示例代码
- Java调用WebService之Axis实现
- Mybatis笔记二
- 如何在Windows平台上基于github搭建个人博客平台
- [干货]深入浅出LSTM及其Python代码实现
- SpringBoot整合MyBatis原理
- 还在手写LaTeX表格?你可能需要这款神器
- 【V-REP自学笔记(三)】用代码控制机器人
- 简单易懂的Docker基础知识
- 【V-REP自学笔记(四)】键盘控制YouBot机器人
- SSM整合步骤
- 【V-REP自学笔记(五)】YouBot底盘运动学与路径规划
- 【V-REP自学笔记(六)】基于V-REP逆运动学模块的机械臂轨迹规划
- 【V-REP自学笔记(七)】Matlab/Python远程控制
- 事务隔离级别总结