文章目录
前言
在上一篇文章中,已介绍了线程池ThreadPoolExecutor
的概念,运行流程,注意事项以及实战,以及详细拆解分析了线程池任务提交方法execute()。
ThreadPoolExecutor
类本身是没有submit方法的,但其继承了AbstractExecutorService
这个线程池抽象类,这个抽象类呢又实现了ExecutorService
接口,submit方法,正是ExecutorService
接口中的抽象类,这一点,在上篇博文类图分析中也有展示
具体可查看博文:万字详解-JAVA线程池分析实战与利弊详解
从类图得知,submit()一系列方法,属于线程池次顶层接口ExecutorService
的抽象方法,且该接口与execute()方法不同的是其不仅可以接收Runnable
、Callable
甚至还可以有返回值。
通读了上一篇文章后,我们应该清楚,线程池execute方法提交的任务执行后是没有返回值的,那么这个submit居然可以获取返回值,是如何实现的呢?我们带着疑问,走进源码。
submit方法定义
submit是接口ExecutorService
中的几个重载抽象方法
参数为callable的方法,返回一个Future
<T> Future<T> submit(Callable<T> task);
参数为Runnable 和预设结果 的方法,返回一个Future(runnable执行结束后,返回设置的result)
<T> Future<T> submit(Runnable task, T result);
参数为Runnable的方法,返回一个Future
Future<?> submit(Runnable task);
callable和runable一样,都是一个函数式接口(都可以使用Lambda表达式来实现方法)且都是实现线程的方式,区别就是callable可以获取返回值以及抛出异常
那我们如何界定我们调用的submit(Callable task) 方法 还是 submit(Runnable task)方法呢?
很简单,就看我们的lambda代码段逻辑中是否含有返回值就行了
ex:
() -> System.out.println(111) ,这段lambda代码块没有返回值,故此调用的是submit(Runnable task)
Future<?> submit = threadPool.submit(() -> System.out.println(111));
() -> 111 ,这段lambda代码块有返回值,故此调用的是submit(Callable task)
Future<Integer> submit = threadPool.submit(() -> 111);
Future是什么
点击源码查看,Future也是一个接口,其有五个抽象方法,分别是取消任务 、判断是否已取消任务、判断任务是否完成、获取任务结果(一直阻塞等待直至获取到结果)、指定时间内获取任务结果
其有这169+具体实现类,后边我们根据线程池submit的使用,再仔细剖析其中的个别具体实现
submit主干流程逻辑
ExecutorService接口的实现类有如下
因AbstractExecutorService
是ThreadPoolExecutor
的父类,且ThreadPoolExecutor
没有再次覆写submit相关方法,因此我们用 ThreadPoolExecutor
中submit系列方法,实际就是使用的父类 AbstractExecutorService
中的方法,所以我们故此我们从这里入手
下方是submit的3个实现
其主干流程很简洁明了
1 根据 Runnable 或 Callable构建RunnableFuture
2 execute执行逻辑
3 返回 RunnableFuture
newTaskFor做了什么
newTaskFor 是将我们的Runnable 或者 Callable 转换为Future或Future的实现,在AbstractExecutorService 中是转为了FutureTask
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {}
FutureTask一种可取消的异步计算类。这个类提供了Future的基本实现,其中包含启动和取消计算、查询计算是否完成以及检索计算结果的方法
下方是FutureTask 这个类的方法展示以及树结构图,我们先简单过一下类中的方法与类结构图,后边会继续详解
上边有一些关键字段信息
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
// 要执行的任务,执行完后会清理
private Callable<V> callable;
// 任务执行的结果 (也可能是异常)
private Object outcome;
private volatile Thread runner;
private volatile WaitNode waiters;
}
newTaskFor(Runnable runnable, T value)的实现
做的事情主要是有两个
1、将runnable转换为callable
调用了Executors.callable方法,将 runnable转换为callable
其实际就是继续将Runnable 使用 RunnableAdapter包裹了一下
自定义了一个RunnableAdapter类去实现 Callable,内部依然存的是runnable,和参数传入的预设结果
2、设置FutureTask的状态
设置FutureTask状态为新建
FutureTask(Callable callable)的实现
做的事情也是两个
1、将我们的callable设置到FutureTask的字段
2、设置FutureTask状态为新建
execute(ftask)
execute方法,调用的是顶层Executor
类中execute (Runnable command)方法
那如果使用ThreadPoolExecutor submit的话则实际就是使用 ThreadPoolExecutor类中 execute方法
那么execute执行的就是 execute(futureTask)
ThreadPoolExecutor类中 execute方法执行逻辑 可以参考我的上篇博文:万字详解-JAVA线程池分析实战与利弊详解
问题就来了,ThreadPoolExecutor执行execute是没有返回值的,我们使用FutureTask包了一下Runnable或者Callable就可以拿到返回值呢?
谜底就在谜面上,FutureTask内部逻辑为我们做了处理
FutureTask是如何实现线程池执行可获取返回值的
还记得上方的FutureTask类结构图吗? FutureTask就是一个Runnable的子类
而我们线程池执行execute会先创建Worker,然后执行Worker
Worker是啥?Worker不也是Runnable的一个包装么,最终都会执行run()方法,我们的FutureTask也一样,在线程池执行的时候,会调用run()方法
那FutureTask的run方法做了什么事情呢?
run方法
从主干流程来说比较简单 判断状态 > 执行callable > 捕获了异常,将结果设置到outcome >清理工作
public void run() {
// 判断线程状态
if (state != NEW ||
!RUNNER.compareAndSet(this, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
// 结果
V result;
// 是否成功执行
boolean ran;
try {
// 执行call (执行我们的业务逻辑)
result = c.call();
// 设置成功执行标签为true
ran = true;
} catch (Throwable ex)
//特别注意的是,这里捕获了异常
// 设置
result = null;
// 设置成功执行标签为false
ran = false;
// 设置异常信息
setException(ex);
}
if (ran)
// 成功执行了任务,设置结果
set(result);
}
} finally {
// 清理工作,清理线程和状态
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
setException(Throwable t)
这个方法是在FutureTask执行任务时,出现了异常被捕获进入
把Throwable 赋值到了outcome (将异常设置到了结果字段中),并设置执行状态为异常
set(V v)
这个方法是在FutureTask 成功执行后进入,用来为结果字段outcome 赋值,并设置执行状态为正常结束
将结果(或异常)赋值到outcome 后,如何获取?
获取结果,简单来说就是获取outcome字段的值
无超时时间,阻塞当前调用者线程,直至获取到结果,并根据执行状态进行不同逻辑的处理,比如下方的NORMAL,CANCELLED等
![image-20230621155435146](https://i-blog.csdnimg.cn/blog_migrate/c4dcc5fd228b518570e6bfd1a0c2c28b.png)
有超时时间,阻塞当前调用者线程一定时间,时间到了未获取到则为null
![image-20230621155510016](https://i-blog.csdnimg.cn/blog_migrate/e3e48ecee8088b646eb24fbaa9594791.png)
判断任务执行状态,执行状态为NORMAL则返回结果,执行状态为CANCELLED则抛出任务取消的异常,否则抛出异常
execute、submit出现异常情况下不同的处理
下方定义了线程池参数,核心线程数为4 最大线程数也为4,那么理论来讲 我们线程的名字最多从my-thread-1 到 my-thread-4
static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1024), new ThreadFactory() {
final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("my-thread-" + atomicInteger.incrementAndGet());
return thread;
}
}, new ThreadPoolExecutor.AbortPolicy());
execute
在上一篇博文:万字详解-JAVA线程池分析实战与利弊详解 中我们也讲解了execute的执行流程,runWorker是我们线程池任务执行的具体方法,在内部执行run方法,捕获但抛出了异常,然后会结束销毁线程。
测试代码
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName() + "执行业务逻辑");
// 模拟异常
if (finalI % 2 == 0) {
int a = 1 / 0;
}
});
}
TimeUnit.SECONDS.sleep(10);
System.out.println("线程池线程数:" + threadPoolExecutor.getPoolSize());
}
结果:
创建的线程名远远超过了预期my-thread-4,甚至名字到了my-thread-58,这说明我们的线程池在执行的过程中至少创建了58个线程,并在执行线程中(比如图中的 my-thread-1 、my-thread-11)抛出了异常。
最终打印线程池线程数仍然是4个,说明我们最大线程数限制起到了作用,那my-thread-58等这些超过my-thread-4的线程代表着什么呢?代表着其因任务执行失败不断地创建线程(addWorker方法) 以及任务异常后不断地销毁线程(processWorkerExit方法)
submit
在上方我们也讲过了,FutureTask类中 run方法进行了异常捕获,那么其实战中表现情况是怎样的呢?;
我们更改一下上方的测试代码Main方法,使用submit
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
int finalI = i;
Future<?> submit = threadPoolExecutor.submit(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName() + "执行业务逻辑");
// 模拟异常
if (finalI > 98) {
int a = 1 / 0;
}
});
if (finalI == 99) {
// 这里入如果不使用try-catch,那么程序就会直接中断
try {
Object o = submit.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}
TimeUnit.SECONDS.sleep(10);
System.out.println("线程池线程数:" + threadPoolExecutor.getPoolSize());
}
结果:
程序一直在打印线程名 my-thread-1 到 my-thread-4,说明线程池没有因任务失败而不断地创建销毁线程
获取任务结果时,才会抛出异常
get方法捕获了异常:
捕获了异常,不会中断调用者线程
get方法,不捕获异常
get尝试获取结果的时候,没有捕获异常的话,一旦发生了异常则会中断调用者线程
异常情况处理总结
execute
execute在任务执行时,如果出现异常,默认在执行线程中会抛出异常并打印堆栈信息,且会删除执行的线程,本次execute要执行的任务就视为结束了,异常的抛出不会中断调用者线程;
如果提交的任务很多,且抛出了异常的话线程池将会不断地创建线程 销毁线程
submit
submit方法在任务执行时,无论是否出现异常都会将结果存在FutureTask中outcome字段中,且不会打印任何异常堆栈信息,本次submit要执行的任务就视为结束了,只有在调用者线程主动调用get方法,尝试获取任务结果的时候,如果任务执行失败或取消了才会抛出异常,且如果在调用者线程使用get时没有捕获异常,一旦出现异常调用者线程将会因异常抛出而被中断
所以呢,我们在使用线程池执行任务的时候要特别注意异常情况的处理,视情况而定,该捕获捕获,该抛出抛出。不然不断的创建销毁线程程序的性能反而下降或者导致我们的调用者线程已异常抛出提前结束
execute、submit方法异同总结
(1) 一个有返回值一个无返回值
execute方法无返回值
submit方法有返回值
(2) submit方法,底层也是执行的execute方法
exeute是ThreadPoolExecutor线程池方法
submit是较为上层的抽象接口,线程池实现类进行了覆写,将任务包装为FutureTask后调用ThreadPoolExecutor线程池exeute方法
(3) 二者的应用场景不同
execute
侧重于异步任务执行,且与调用线程无联动性,只需要提交了后让线程池慢慢消费就好了,任务的结果与调用者线程无关联和依赖。
流程示例Ex:
调用者线程 > 提交线程池执行异步任务(不关心异步任务是否有返回值以及返回值结果)
场景:
调用者线程进行逻辑校验,校验通过后,提交线程池进行短信发送
submit
既侧重与异步执行,又侧重于获取执行后的结果做逻辑处理 或者多个异步任务之间有着关联性
流程示例Ex:
调用者线程 > 提交线程池执行异步任务(关心异步执行结果)可能提交多个异步任务 > 调用者线程拿到多个异步任务结果后再进行逻辑处理
场景:
查询一个复杂的业务数据(这里假设是驾驶员)
调用者线程 判断是否有这个驾驶员 > 提交线程池执行异步任务(任务1 查询从业证 耗时300ms,任务2 查询车辆信息耗时 300ms,任务3查询驾驶证信息 耗时400ms)>等待所有异步任务执行完毕,调用者线程判断任务结果拼接数据返回前端
(4)应对异常任务时处理情况不同