线程池的鼻祖AbstractExecutorService模板模式真香(上)

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。

【微信公众号】
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

【原】编程界的小学生

没有打赏我依然会坚持。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值