一文搞懂Executor执行器和线程池的关系,整体介绍其任务执行/调度体系:ThreadPoolExecutor、ScheduledExecutorService

精彩的失败远胜于平庸的成功。

前言

本文进行JavaSE基础内容:Executor执行器体系的整体介绍。该文是整体框架介绍,并非局限于某一个使用的细节。由于我不止一次的被咨询说ExecutorServiceScheduledExecutorService什么区别和联系,以及ThreadPoolExecutorThreadPoolTaskExecutor有什么不一样之类的问题,因此决定写此文科普一下。

本文内容不深,但比较全,因此我相信绝对是你值得阅读的一篇文章(即使你可能已经工作了5年+)。

说明:流行框架如Spring、MyBatis、Netflix等一般均对Executor体系有所扩展,本文只讲述JDK的内容,其它的举一反三即可。


正文

java.util.concurrent.Executor它属于J.U.C体系里的重要一员,是在JDK 1.5之后新增的内容,由大名鼎鼎的Doug Lea大神操刀。因为它的出现才使得我们大量的使用多线程开发程序成为了可能。

Executor执行器体系整体的框架可描述为如图所示(只例举出重要的API,覆盖绝大部分场景):

在这里插入图片描述


膜拜Doug Lea大神

喝水不忘挖井人,首先来膜拜一下这位超级大神吧。

Doug Lea中文一般翻译为:道格·利。正所谓编程不识道格·利,写尽Java也枉然,杂志《程序员》甚至评价他为:世界上对Java影响力最大的个人。足以见得他在Java领域的地位。

Doug Lea是真大神,大名鼎鼎的Java并发包(J.U.C)作者,同时也是HashMap作者之一…Java5为何能被称为有史以来最重磅的一次升级?J.U.C并发包功不可没,包括1.7新引入的ForkJoinPool也是另一神作。

他对Java做的贡献是无量的,此人乃真大神也,一起膜拜下吧:

在这里插入图片描述


Executor 执行器

执行器,可执行任意一个Runnable任务。该接口提供了一种将任务提交与如何运行每个任务的机制(包括线程使用、调度等细节)分离的方法。因此任务它自己并不需要关心线程的创建、调度细节。

画外音:可能直接执行,也可能把你交给线程池执行,总之我帮你执行就欧克了

public interface Executor {
	// @since 1.5
	void execute(Runnable command);
}

需要注意的是:该执行器并不规定是同步执行还是异步执行你提交上来的任务,下面分别举例。

画外音:你之前一直以为Executor一定是异步,那是错的。应该说:绝大部分情况下我们会去异步执行,且绝大部分情况下均会使用线程池。但绝大多数并不代表所有


同步执行任务

@Test
public void fun1() {
    String mainThreadName = Thread.currentThread().getName();
    System.out.println("----------主线程[" + mainThreadName + "]开始----------");

    // 自己定义一个同步执行器
    Executor syncExecutor = (Runnable command) -> command.run();

    // 提交任务
    syncExecutor.execute(() -> {
        String currThreadName = Thread.currentThread().getName();
        System.out.println("线程[" + currThreadName + "] 我是同步执行的...");
    });
}

运行程序,控制台打印:

----------主线程[main]开始----------
线程[main] 我是同步执行的...

特点:自己直接显示调用Runnable#run那便是简单的方法调用而已,属于同步。


异步执行任务

@Test
public void fun2() throws InterruptedException {
    String mainThreadName = Thread.currentThread().getName();
    System.out.println("----------主线程[" + mainThreadName + "]开始----------");

    // 自己定义一个异步执行器
    Executor asyncExecutor = (Runnable command) -> new Thread(command).start();

    // 提交任务
    asyncExecutor.execute(() -> {
        String currThreadName = Thread.currentThread().getName();
        System.out.println("线程[" + currThreadName + "] 我是异步执行的...");
    });

    TimeUnit.SECONDS.sleep(1);
}

运行程序,控制台打印:

----------主线程[main]开始----------
线程[Thread-0] 我是异步执行的...

特点:new了一个Thread去执行command任务,调度交由系统去掌控,属于异步。


通过如上两个示例可以看到Executor它并不代表线程池(线程池的英文是:ThreadPool好麽~),仅是任务执行器而已。抽象出这么一个接口是想屏蔽掉内部执行、调度的细节,方面使用者的使用而已。


ExecutorService

Executor过于抽象,仅代表着任务的执行(甚至同步、异步都没规定),因此直接通过实现该顶层接口的类并没有,平时使用得最多还是这个子接口ExecutorService:提供更多的服务。

// @since 1.5
public interface ExecutorService extends Executor {

	// 关闭。执行已经提交的任务,但不接受新任务。
	// 此方法用于关闭不需要使用的执行器,内部会做资源回收的操作,如回收线程池
	void shutdown();
	// 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表
	// 此方法一般不使用
	List<Runnable> shutdownNow();
	// 执行器是否已经被关闭
	boolean isShutdown();
	// 只有当shutdown()或者shutdownNow()被调用,而且所有任务都执行完成后才会返回true
	boolean isTerminated();
	// 阻塞。直到所有的任务都被执行完,或者超时
	boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

	// =====下面为任务提交方法(使用Future跟踪任务执行情况)=====
	// 以下三个是最最最最最最最常用的方法:执行任务
	// Callable任务有返回值。所以Future.get()的时候可以拿到此返回值
	<T> Future<T> submit(Callable<T> task);
	// Runnable任务。执行完后Future.get()永远返回的是result这个值
	<T> Future<T> submit(Runnable task, T result);
	// 执行完后Future.get()永远返回的是null
	Future<?> submit(Runnable task);

	// 这几个方法在**批量执行**或**多选一**的业务场景中非常方便。
	// invokeAll:所有任务都完成(包括成功/被中断/超时)后才会返回,所以它会阻塞哦(时间由最长的决定)
	// invokeAny()在任意一个任务成功(被中断/超时)后就会返回,只需要成功一个就返回(时间由最短的决定)
	<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
	<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;
}

其实提交的Runnable任务最终都会通过Executors.callable(runnable, result)适配为一个Callable<V>去执行的,具体源码处有兴趣的可以看一看。

从类图中可以看出,ExecutorService的实现分为两个分支:左边的AbstractExecutorService(一般为基于线程池的实现),以及右边的ScheduledExecutorService延迟/周期执行服务,下面也得分别作出解释。

画外音:submit/invokeAll等方法如何去执行任务,调用execute()放入线程池or周期性执行,或者结合在一起?这是它的两大方向


AbstractExecutorService

ExecutorService的抽象实现:它实现了接口的部分方法,但是它并没有存放任务或者线程的数组或者Collection,也就是说它依旧和线程池没有半毛钱关系

// @since 1.5
public abstract class AbstractExecutorService implements ExecutorService {

	@Override
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    ... // 其它submit方法原理一样,底层执行调用的均是Executor#execute方法
}

关于invokeAll()/invokeAny()等方法的执行源码此处就不铺开了,记住结论即可:批量执行时使用特别方便(注意全部成功or任意一个成功的区别)。


手写实现AbstractExecutorService

本文以一个非常简单手写实例来告诉你任务执行器的效果,进一步让你感受到它目前还和线程池木有半毛钱关系

private static class MyExecutorService extends AbstractExecutorService {

    @Override
    public void shutdown() {
        System.out.println("关闭执行器,释放资源");
    }
    @Override
    public List<Runnable> shutdownNow() {
        System.out.println("立刻关闭执行器,释放资源");
        return Collections.emptyList();
    }
    @Override
    public boolean isShutdown() {
        return false;
    }
    @Override
    public boolean isTerminated() {
        return false;
    }
    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return false;
    }
    
    // 执行任务(本处使用异步执行)
    @Override
    public void execute(Runnable command) {
        new Thread(command).start();
    }
}

准备一个方法,用于产生任务:

// period:任务执行耗时 单位s
private Runnable createTask(int period) {
    return () -> {
        try {
            TimeUnit.SECONDS.sleep(period);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String currThreadName = Thread.currentThread().getName();
        System.out.println("线程[" + currThreadName + "] 我是异步执行的,耗时" + period + "s");
    };
}

书写测试方法:

@Test
public void fun3() throws InterruptedException, ExecutionException {
    String mainThreadName = Thread.currentThread().getName();
    System.out.println("----------主线程[" + mainThreadName + "]开始----------");

    ExecutorService executorService = new MyExecutorService();

    Instant start = Instant.now();
    Future<?> submit = executorService.submit(createTask(3));
    System.out.println("结果为:" + submit.get());
    Instant end = Instant.now();
    System.out.println("总耗时为:" + Duration.between(start, end).getSeconds());

    executorService.shutdown();
}

运行程序,打印:

----------主线程[main]开始----------
线程[Thread-0] 我是异步执行的,耗时3s
结果为:null
总耗时为:0
关闭执行器,释放资源

下面仅需改一下,把get放在上面:

System.out.println("结果为:" + submit.get());
Instant end = Instant.now();
System.out.println("总耗时为:" + Duration.between(start, end).getSeconds());

再次运行打印:

----------主线程[main]开始----------
线程[Thread-0] 我是异步执行的,耗时3s
结果为:null
总耗时为:3
关闭执行器,释放资源

另外,关于其它submit方法,以及批量执行的invokeAll/invokeAny方法,各位可自行测试哈。下面介绍JDK自带的,Doug Lea大神给我们提供的实现类,也是最最最最最为重要的一个类:ThreadPoolExecutor


ThreadPoolExecutor 带线程池的执行器

顾名思义,它是一个内置线程池的执行器,也就是说:它会把Runnable任务的执行均扔进线程池里面进行执行,效率最高。

注意:ThreadPoolTaskExecutor它是Spirng提供的,基于ThreadPoolExecutor进行包装实现,请勿弄混了。

本文并不会解释为何需要线程池,以及构建线程池的七大参数都是什么意思,而只会站在使用以及基础原理的角度做出示例和说明。

public class ThreadPoolExecutor extends AbstractExecutorService {
	
	// 核心线程数
	private volatile int corePoolSize;
	// 最大线程数
	private volatile int maximumPoolSize;
	// 任务队列(当任务太多了,就放在这里排队)
	private final BlockingQueue<Runnable> workQueue;
	// 空闲线程的超时时间,超时就回收它(非core线程)
	private volatile long keepAliveTime;
	// 不解释
	private volatile ThreadFactory threadFactory;
	// 线程池拒绝处理函数(如任务太多了,如何拒绝)
	// 默认策略是:AbortPolicy。要决绝是抛出异常:RejectedExecutionException
	private volatile RejectedExecutionHandler handler;
	
}

通过上里手工模拟可知道最重要的是对execute()方法的实现:它决定了你最终如何去执行任务(同步or异步?用老的线程还是用新的线程等等)。

execute()方法执行分析
ThreadPoolExecutor:

    public void execute(Runnable command) {
        if (command == null) throw new NullPointerException();
        
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        } else if (!addWorker(command, false))
            reject(command);
    }

这段代码实际运作非常的复杂,但因为大神对其良好的封装,使得我们理解起来并不难。对提交来的新任务处理步骤用一张图描绘如下:

在这里插入图片描述

对于不需要返回值的任务,使用submit or execute效果一样,但一般情况下推荐统一使用更高层的submit系列方法去提交你的任务。


代码示例

略(相信没有人不会用它吧)。


ScheduledExecutorService

它在ExecutorService接口上再扩展,额外增加了定时、周期执行的能力。

public interface ScheduledExecutorService extends ExecutorService {
	
	// ========这个两个方法提交的任务只会执行一次========
	// 创建并执行启用的一次性操作在给定的延迟之后。
    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
    // 创建并执行启用的一次性操作在给定的延迟之后。
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);


	// ========这个两个方法提交的任务会周期性的执行多次========
	// 在给定的初始延迟之后,以给定的时间间隔执行周期性动作。
	// 即在 initialDelay 初始延迟后initialDelay + period 执行第一次
	// initialDelay + 2 * period  执行第二次,依次类推
	// 特点:下一次任务的执行并不管你上次任务是否执行完毕
	// 所以它名叫FixedRate:固定的频率执行(每次都是独立事件)
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
	// 给定的初始延迟之后首先启用的定期动作
	// 随后**上一个执行的终止**和**下一个执行的开始之间**给定的延迟。
	// 也就是说delay表示的是上一次的end和下一次start之间的时间,两次之间是有关系的,不同于上个方法
	// 所以它名叫FixedDelay:两次执行间固定的延迟
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

针对此接口,非常有必要强调如下几点:

  1. 定时执行和周期性执行是两码事
  2. 通过ScheduledExecutorService#schedule()方法调度执行的任务有且仅会执行一次(当然你任务内部怎么去玩就不归ScheduledExecutorService管喽)
    1. 注意:据我了解这是很多小伙伴的误区,以为ScheduledExecutorService执行的任务都会周期性执行的,这是非常错误的理解哦

还需要提示一点的是:该接口依旧和线程池木有关系,主要看子类如何去实现。而大神给我们提供为“唯一”实现:ScheduledThreadPoolExecutor


ScheduledThreadPoolExecutor 集大成者

它可谓线程池 + 执行器的集大成者,最强子类:在线程池里执行任务,并且还可以定时、周期性的执行

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService { ... }

代码示例

略。


关于它有话说:如果你有定时、周期执行任务的需求,请使用ScheduledThreadPoolExecutor来代替Java古老的Timer/TimerTask API吧。

关于它俩的对比,可参考这篇文章:Java定时任务ScheduledThreadPoolExecutor详解以及与Timer、TimerTask的区别(执行指定次数停止任务)


关于Executors

通过命名可知它是用于创建Executor执行器的工具类(均为静态方法):

  • 可快捷创建带有线程池能力的执行器ThreadPoolExecutor(ExecutorService)
  • 可快速创建具有线程池,且定时、周期执行任务能力的ScheduledThreadPoolExecutor(ScheduledExecutorService)

另外,它还提供几个我认为比较好用的工具方法分享给大家:

Executors:

	// 默认线程工厂:
	// 线程名称为:pool-[poolNum]-thread-[threadNumber]
	// 非守护线程(请注意:非守护线程,看看是否符合你的要求)
    public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }

	// 很方便的把一个Runnable适配为一个Callable<T>
	// result可以是个常量值。当然也可以是null
    public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
    public static Callable<Object> callable(Runnable task) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<Object>(task, null);
    }

关于Executors最后我想说:在生产环境下禁用,禁用,禁用(原因请参照阿里编码规范),一般用于自己测试的时候快速构建方便而为之。

使用线程池,请务必对其七大参数烂熟于胸,否则不要使用,容易酿成大祸或,并且还是软病,很难排查,因此需要敬畏和谨慎。


总结

关于Java中的Executor执行器大体系,以及它和线程池是什么关心就介绍到这,我相信经过本文你应该能彻底了解该体系的框架了吧,不用每次都不知道使用哪个了。

ScheduledExecutorService属于最强接口,它具有全部能力,不过一般若你并不需要定时/周期执行能力的时候,请使用ThreadPoolExecutor/ExecutorService即可。


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

往期精选

YourBatman CSDN认证博客专家 博客专家 专栏创作者 BAT的乌托邦
也许当我老了,也一样写代码。不为别的,只为爱好
公众号:BAT的乌托邦
亦可在这里和我交流:https://www.yourbatman.cn
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付 29.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值