文章目录
author:编程界的小学生
date:2021/05/07
flag:不写垃圾没有营养的文章!
如果下面的问题你都会的话就别在这浪费时间啦
- Runnable和Callable的区别?
- 一直用的ThreadPoolExecutor真的是线程池的顶级入口吗?
- submit和execute的区别?
- submit(Runnable r)是怎么带返回值的?
- submit是怎么吞掉异常信息的?
- 线程池完美的采取了模板方法设计模式你看到了吗?
- shutdown和shutdownNow都属于ThreadPoolExecutor吗?有啥区别?
1、面试题:Runnable和Callable的区别
为什么需要先说下Runnable和Callable的区别?因为线程池里的任务就是这两个,所以有必要先知道这两个啥区别。
这也是常考面试题之一。
- Callable重写的方法是call(),Runnable重写的方法是run()
- Callable的任务执行后带返回值,Runnable的任务是没有返回值的
- call()可以抛出异常,run()不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。他提供了检查计算是否完成的方法,以等待计算的完成并检索计算的结果,通过Future对象可以了解任务执行情况,也可以取消任务的执行,还可以获取任务执行的反馈结果。Runnable是个黑盒,执行后成功与否或者想中途取消任务都是不存在的。
2、线程池完整体系
- Executor:线程池顶级接口
- ExecutorService:线程池次级接口,对Executor做了一些扩展,增加了一些功能
- ScheduledExecutorService:对ExecutorService做了一些扩展,增加了一些定时任务相关的功能
- AbstractExecutorService:抽象类,运用模板方法设计模式实现了一部分方法(模板框架方法,很经典!)
- ThreadPoolExecutor:普通线程池类,包含最基本的一些线程池操作相关的方法的具体实现
- ScheduledThreadPoolExecutor:定时任务线程池类,用于实现定时任务相关功能
- ForkJoinPool:新型线程池类,Java7中新增的线程池类,基于工作窃取理论实现,运用大任务拆分小任务,任务无限多的场景
- Executors:线程池工具类,定义了一些快速实现线程池的方法
3、Executor
// 线程池顶级接口,定义了一个执行无返回值任务的方法
public interface Executor {
/**
* 执行无返回值任务
* 根据Executor的实现判断,可能是在新线程、线程池、线程调用中执行
*/
void execute ( Runnable command);
}
所以我们一直在用的执行任务execute其实是属于Executor这个接口的。具体实现交由各个子类。所以ThreadPoolExecutor不是线程池顶级入口,Executor接口才是。
4、ExecutorService
public interface ExecutorService extends Executor {
// 关闭线程池,不再接受新任务,但已经提交的任务会执行完成
void shutdown();
/**
* 立即关闭线程池,尝试停止正在运行的任务,未执行的任务将不再执行
* 被迫停止及未执行的任务将以列表的形式返回
*/
List<Runnable> shutdownNow();
// 检查线程池是否已关闭
boolean isShutdown();
// 检查线程池是否已终止,只有在shutdown()或shutdownNow()之后调用才有可能为true
boolean isTerminated();
// 在指定时间内线程池达到终止状态了才会返回true
boolean awaitTermination ( long timeout, TimeUnit unit) throws InterruptedException;
// 执行有返回值的任务,任务的返回值为task.call()的结果
<T> Future<T> submit ( Callable<T> task);
/**
* 执行有返回值的任务,任务的返回值为这里传入的result
* 当然只有当任务执行完成了调用get()时才会返回
*/
<T> Future<T> submit ( Runnable task, T result);
/**
* 执行有返回值的任务,任务的返回值为null
* 当然只有当任务执行完成了调用get()时才会返回
*/
Future<?> submit (Runnable task);
// 批量执行任务,只有当这些任务都完成了这个方法才会返回
<T> List<Future<T>> invokeAll ( Collection<? extends Callable<T>> tasks) throws InterruptedException;
/**
* 在指定时间内批量执行任务,未执行完成的任务将被取消
* 这里的timeout是所有任务的总时间,不是单个任务的时间
*/
<T> List<Future<T>> invokeAll ( Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
// 返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny ( Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
// 在指定时间内如果有任务已完成,则返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny ( Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}
}
ExecutorService算是对Executor的一个补充接口,Executor定义了顶层核心execute方法,但是shutdown/shutdownNow、submit等方法都是定义在ExecutorService里的。所以我们常用的shutdown等api其实是隶属于ExecutorService接口的。
4.1、shutdown和shutdownNow区别
- shutdown只是将线程池的状态设置为SHUTDOWN状态,正在执行的任务会继续执行下去,没有被执行的任务则被中断。
- shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行的任务则被返回。
- 当再将执行execute提交任务时,如果测试到状态不为RUNNING,则抛出rejectedExecution,从而达到阻止新任务提交的目的,这也是上面两个api设置为SHUTDOWN和STOP的意义所在。
5、ScheduledExecutorService
对ExecutorService做了一些扩展,增加一些定时任务相关的功能,主要包含两大类:执行一次,重复多次执行。
public interface ScheduledExecutorService extends ExecutorService {
/**
* 在指定的延时后执行一次
* 参数:delay 从现在开始到延时执行的时间 unit 延时时间单位
*/
public ScheduledFuture<?> schedule ( Runnable command, long delay, TimeUnit unit);
// 在指定的延时后执行一次
public <V> ScheduledFuture<V> schedule ( Callable<V> callable, long delay, TimeUnit unit);
/**
* 在指定延时后首先执行一次,随后按照周期执行,不包含任务执行的时间
* 参数:initialDelay 第一次执行时间 period 两次执行的时间间隔,不包含上一个任务执行时间
* 第一次执行时间:initialDelay
* 第二次执行时间:initialDelay + period
* 第三次执行时间:initialDelay + 2 * period
*/
public ScheduledFuture<?> scheduleAtFixedRate ( Runnable command, long initialDelay, long period, TimeUnit unit);
/**
* 在指定延时后首先执行一次,随后按照指定延时重复执行,相当于包含任务执行的时间
* 参数: initialDelay 第一次延时执行的时间
* delay 一个任务执行结束到另一个任务开始执行之间的延迟,延时以上一个任务结束开始计算
*/
public ScheduledFuture<?> scheduleWithFixedDelay ( Runnable command, long initialDelay, long delay, TimeUnit unit);
}
6、AbstractExecutorService
AbstractExecutorService是Executor和ExecutorService两个接口的具体实现,采取了模板方法设计模式,封装了基础的模板框架,不同线程池的执行策略交由子类来实现。所以其他线程池的鼻祖就是AbstractExecutorService了!
6.1、先来看几个核心API
// 执行有返回值的任务,任务的返回值为task.call()的结果
<T> Future<T> submit (Callable<T> task);
/**
* 执行有返回值的任务,任务的返回值为这里传入的result
* 当然只有当任务执行完成了调用get()时才会返回
*/
<T> Future<T> submit (Runnable task, T result);
/**
* 执行有返回值的任务,任务的返回值为null
* 当然只有当任务执行完成了调用get()时才会返回
*/
Future<?> submit (Runnable task);
// 批量执行任务,只有当这些任务都完成了这个方法才会返回
<T> List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks) throws InterruptedException;
/**
* 在指定时间内批量执行任务,未执行完成的任务将被取消
* 这里的timeout是所有任务的总时间,不是单个任务的时间
*/
<T> List<Future<T>> invokeAll (Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
// 返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny (Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
// 在指定时间内如果有任务已完成,则返回任意一个已完成任务的执行结果,未执行完成的任务将被取消
<T> T invokeAny (Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}
6.2、submit(T t)
三个submit方法大同小异,核心逻辑如下:
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
大概逻辑已经很清晰了:
-
1.将task(Runnable/Callable)包装成RunnableFuture。
-
2.模板方法设计模式,交由自类来完成具体的执行。比如
java.util.concurrent.ThreadPoolExecutor#execute();
-
3.返回结果
6.2.1、newTaskFor
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
表面看很简单,就是把Callable包装成FutureTask返回。但如果是Runnable呢?表面只调用了一个FutureTask的构造函数,其实内部采取了适配器模式来完成的。
6.2.2、总结submit
- 将任务包装成RunnableFuture
- 具体执行采取模板设计模式交由子类来完成
- 最终返回执行结果
6.2.3、面试题:submit和execute的区别
- submit是定义在父类AbstractExecutorService里的,execute是定义到各个子类里的,比如ThreadPoolExecutor
- submit带返回值,execute没返回值
- submit内部是个模板设计模式,最终执行也是调用execute方法
- submit可以接收Runnable/Callable,execute只能接收Runnable
- execute执行抛出异常的话会直接打印到控制台,submit执行报错的话不会将错误信息打印到控制台,而是把异常放到一个成员变量里保存起来,当你用返回值FutureTask.get()获取的结果的时候再把异常抛出来
- execute报错的话线程直接死了,submit报错的话会保存异常,线程没死
6.3.3、面试题:submit(Runnable r)是怎么带返回值的?
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
源码如上,如果只传递Runnable的话,返回值就是null,当然也可以自己传递返回值对象,然后内部调用newTaskFor将Runnable和Result包装成RunnableFuture,也就是FutureTask(因为FutureTask是RunnableFuture的子类)。FutureTask内部采取的是适配器模式将Runnable和result包装成FutureTask的(可以自己点进去看看)。
6.3.4、面试题:submit是怎么吞掉异常信息的?
execute(ftask);
因为submit最终执行的是FutureTask,所以看下FutureTask源码:
java.util.concurrent.FutureTask#run
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);
}
} finally {
...
}
}
重点就在上面的catch,捕获到异常后不抛出,而是放到一个全局变量里存储,然后当get的时候在进行打印出来,get()里会根据条件调用report()进行抛出异常。
7、总结
你应该学会了如下知识:
-
线程池的完整体系结构
-
submit是个模板方法,具体的执行还是调用的execute方法
-
Executor是顶层接口,ExecutorService是对顶层接口的一些补充,而AbstractExecutorService是对ExecutorService接口的一些具体实现,大量采取了模板设计模式。
-
我们常用的线程池都是继承AbstractExecutorService来完成execute的执行逻辑的。都可以看作为模板子类
-
Runnable和Callable的区别
-
submit和execute的区别
-
submit(Runnable r)是怎么带返回值的
下一篇福会讲解invokeAll和invokeAny。
【微信公众号】