Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)

时间:2021-10-03 18:36:28

一、介绍

基于Executor接口中将任务提交和任务执行解耦的设计,ExecutorService和其各种功能强大的实现类提供了非常简便方式来提交任务并获取任务执行结果,封装了任务执行的全部过程。本文尝试通过对该部分源码的解析以ThreadPoolExecutor为例来追踪任务提交、执行、获取执行结果的整个过程。为了避免陷入枯燥的源码解释,将该过程和过程中涉及的角色与我们工作中的场景和场景中涉及的角色进行映射,力图生动和深入浅出。
1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。
经过这样的封装,对于使用者来说,提交任务获取结果的过程大大简化,调用者直接从提交的地方就可以等待获取执行结果。而封装最大的效果是使得真正执行任务的线程们变得不为人知。有没有觉得这个场景似曾相识?我们工作中当UM(UM是unit manager的简写,UM管理TeamLeader)把一个任务交给TeamLeader(TeamLeader,直接管理程序员)的时候,到底是TeamLeader自己干,还是转过身来拉来一帮苦逼的兄弟加班加点干,那UM是不管的。UM只用把人描述清楚提及给TeamLeader,然后喝着咖啡等着收TeamLeader的report即可。等TeamLeader一封邮件非常优雅地报告UM report结果时,实际操作中是码农A和码农B干了一个月,还是码农ABCDE加班干了一个礼拜,大多是不用体现的。这套机制的优点就是UM找个合适的TeamLeader出来提交任务即可,接口友好有效,不用为具体怎么干费神费力。

二、一个简单的例子

看上去这个执行过程是这个样子。UM所需要干的所有事情就是找到一个合适的TeamLeader,提交任务就好了。
package com.npf.test;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class App {

public static void main(String[] args) throws Exception {

//创建一个UM
UM um = new UM("Steve");

//创建一个teamLeader,管7个人的小团队的老大
ExecutorService teamLeaderA = Executors.newFixedThreadPool(7);

//将这个teamLeader给Steve这个UM
List<ExecutorService> teamLeaderList = new ArrayList<ExecutorService>();
teamLeaderList.add(teamLeaderA);
um.setTeamLeaderList(teamLeaderList);

//创建一个任务
MyTask mytask = new MyTask();

//现在Steve这个UM开始向teamLeaderA提交任务了
Future<String> future = um.getTeamLeaderList().get(0).submit(mytask);

//提交后就等着结果吧,到底是手下7个作业中谁领到任务了,teamLeader是不关心的。
String output = future.get();

//输出结果
System.out.println(output);
}

}
使用上非常简单,其实只有两行语句来完成所有功能:创建一个线程池,提交任务并等待获取执行结果。例子中生成线程池采用了工具类Executors的静态方法。除了newFixedThreadPool可以生成固定大小的线程池,newCachedThreadPool可以生成一个*、可以自动回收的线程池,newSingleThreadScheduledExecutor可以生成单个线程的线程池。newScheduledThreadPool还可以生成支持周期任务的线程池。一般用户场景下各种不同设置要求的线程池都可以这样生成,不用自己new一个线程池出来。

三、代码剖析

这套机制怎么用,上面两句语句就做到了,非常方便和友好。但是submit的task是怎么被执行的?是谁执行的?如何做到在调用的时候只有等待执行结束才能get到结果。这些都是1.5之后Executor接口下的线程池、Future接口下的可获得执行结果的的任务,配合AQS和原有的Runnable来做到的。在下文中我们尝试通过剖析每部分的代码来了解Task提交,Task执行,获取Task执行结果等几个主要步骤。为了控制篇幅,突出主要逻辑,文章中引用的代码片段去掉了异常捕获、非主要条件判断、非主要操作。文中只是以最常用的ThreadPoolExecutor线程池举例,其实ExecutorService接口下定义了很多功能丰富的其他类型,有各自的特点,但风格类似。本文重点是介绍任务提交的过程,过程中涉及的ExecutorService、ThreadPoolExecutor、AQS、Future、FutureTask等只会介绍该过程中用到的内容,不会对每个类都详细展开。

1、 任务提交

从类图上可以看到,接口ExecutorService继承自Executor。不像Executor中只定义了一个方法来执行任务,在ExecutorService中,正如其名字暗示的一样,定义了一个服务,定义了完整的线程池的行为,可以接受提交任务、执行任务、关闭服务。抽象类AbstractExecutorService类实现了ExecutorService接口,也实现了接口定义的默认行为。
Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)
AbstractExecutorService任务提交的submit方法有三个实现。第一个接收一个Runnable的Task,没有执行结果;第二个是两个参数:一个任务,一个执行结果;第三个一个Callable,本身就包含执任务内容和执行结果。 submit方法的返回结果是Future类型,调用该接口定义的get方法即可获得执行结果。 V get() 方法的返回值类型V是在提交任务时就约定好了的。
除了submit任务的方法外,作为对服务的管理,在ExecutorService接口中还定义了服务的关闭方法shutdown和shutdownNow方法,可以平缓或者立即关闭执行服务,实现该方法的子类根据自身特征支持该定义。在ThreadPoolExecutor中,维护了RUNNING、SHUTDOWN、STOP、TERMINATED四种状态来实现对线程池的管理。线程池的完整运行机制不是本文的重点,重点还是关注submit过程中的逻辑。
(1) 看AbstractExecutorService中代码提交部分,构造好一个FutureTask对象后,调用execute()方法执行任务。我们知道这个方法是*接口Executor中定义的最重要的方法。FutureTask类型实现了Runnable接口,因此满足Executor中execute()方法的约定。同时比较有意思的是,该对象在execute执行后,就又作为submit方法的返回值返回,因为FutureTask同时又实现了Future接口,满足Future接口的约定。
Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)

(2) Submit传入的参数都被封装成了FutureTask类型,然后将FutureTask类型的参数传入execute方法,并执行。对应前面三个不同的参数类型都会封装成FutureTask。
Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)
(3) Executor接口中定义的execute方法的作用就是执行提交的任务,该方法在抽象类AbstractExecutorService中没有实现,留到子类中实现。我们观察下子类ThreadPoolExecutor,使用最广泛的线程池如何来execute那些submit的任务的。这个方法看着比较简单,但是线程池什么时候创建新的作业线程来处理任务,什么时候只接收任务不创建作业线程,另外什么时候拒绝任务。线程池的接收任务、维护工作线程的策略都要在其中体现。
作为必要的预备知识,先补充下ThreadPoolExecutor有两个最重要的集合属性,分别是存储接收任务的任务队列和用来干活的作业集合。
Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)
其中阻塞队列workQueue是来存储待执行的任务的,在构造线程池时可以选择满足该BlockingQueue 接口定义的SynchronousQueue、LinkedBlockingQueue或者DelayedWorkQueue等不同阻塞队列来实现不同特征的线程池。 关注下execute(Runnable command)方法在ThreadPoolExecutor中的实现源码:
  /**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
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);
}
从源码的注释中,我们可以看出:
Executes the given task sometime in the future.  The task may execute in a new thread or in an existing pooled thread.If the task cannot be submitted for execution, either because this executor has been shutdown or because its capacity has been reached,the task is handled by the current {@code RejectedExecutionHandler}.
简单翻译过来就是说:对于给定的任务,将会在未来的某个时间被执行。这个任务可能是被一个新线程执行,又或者是线程池里面已有的线程执行。如果这个任务在本次的执行环境中不能被提交,可能是当前的线程池已经shutdown, 又或者是当前线程池的容量已经达到极限,那么这个任务就会被当前的RejectedExecutionHandler所处理。 至于源代码里面的注释则是重中之重,它描述的是任务是如何加入当前线程池中的。 1. If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task.  The call to addWorker atomically checks runState and workerCount, and so prevents false alarms that would add threads when it shouldn't, by returning false. 简单翻译过来就是说:如果当前线程池中正在运行的线程小于corePoolSize ,那么对于新提交的任务,我们会创建一个新的线程来处理这个任务。对于调用addWorker这个方法,目的就是去原子性的去检查runStateh和workerCount这两个属性,避免出现错误的警告。换句话说,如果线程池的状态是RUNNING,线程池的大小小于核心线程数,说明还可以创建新线程,则启动新的线程执行这个任务。

Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)

2. If a task can be successfully queued, then we still need to double-check whether we should have added a thread (because existing ones died since last checking) or that the pool shut down since entry into this method. So we recheck state and if necessary roll back the enqueuing if stopped, or start a new thread if there are none. 简单翻译过来就是说:如果这个任务可以顺利的被放入到任务阻塞队列中,我们依旧需要double-check一下当前线程池是否还可以添加新线程,因为从上一次检查之后,池中的线程有可能会存在死掉的线程,又或者是线程池shut down了,因为有可能调用了shut down的方法。所以我们需要去再次检查线程池的状态,如果有需要的话,我们需要停止入对列,并开启一个新的线程去执行刚提交的任务。换句话说,如果线程池的状态是RUNNING ,线程池的大小大于核心线程数,并且小于最大线程数,如果任务队列不满,只需把任务加入任务队列即,则提交的任务在任务队列中等待处理。如果任务队列已经满了,说明现有线程已经不能支持当前的任务了,并且线程池还有继续扩充的空间,就可以创建一个新的线程来处理提交的任务。
Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)

3. If we cannot queue task, then we try to add a new thread.  If it fails, we know we are shut down or saturated and so reject the task. 简单翻译过来就是说:如果任务不能放入任务阻塞队列,那么就创建一个新线程来处理。如果失败了,那么我们就拒绝这个任务。换句话说,如果线程池的状态是RUNNING ,线程池的大小大于最大线程数,并且任务队列已经满了,又或者是当线程池已经关闭或者上面的条件都不能满足时,则进行拒绝策略,拒绝策略在RejectedExecutionHandler接口中定义,可以有多种不同的实现。

Java Executor并发框架(六)Executor框架线程池任务执行全过程(上)
上面其实也是对最主要思路的解析,详细展开可能还会更复杂。简单梳理下思路:构建线程池时定义了一个额定大小,当线程池内工作线程数小于额定大小,有新任务进来就创建新工作线程,如果超过该阈值,则一般就不创建了,只是把接收任务加到任务队列里面。但是如果任务队列里的任务实在太多了,那还是要申请额外的工作线程来帮忙。如果还是不够用就拒绝服务。
至此,任务提交过程简单描述完毕,并介绍了任务提交后ExecutorService框架下线程池的主要应对逻辑,其实就是接收任务,根据需要创建或者维护管理线程。

参考文献:

1.  戏(细)说Executor框架线程池任务执行全过程(上)