一.前言
Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。经过这样的封装,对于使用者来说,提交任务获取结果的过程大大简化,调用者直接从提交的地方就可以等待获取执行结果。
二.简单例子
ExecutorService pool = Executors.newFixedThreadPool(7);
//提交作业,作业内容封装在Callable中,约定好了输出的类型是String。
Future result = pool.submit(new Callable<String>() {
public String call() throws Exception
{
return "我是一个要被线程执行的任务!";
}
});
String outputs = result.get().toString();
System.out.println(outputs);
三.源码分析
主要步骤:Task提交,Task执行,获取Task执行结果。
大体过程:
构建线程池时定义了一个额定大小,当线程池内工作线程数小于额定大小,有新任务提交就创建新工作线程,如果超过该阈值,把接收任务加到任务队列里面。
但是如果任务队列里的任务实在太多了,那还是要申请额外的工作线程来帮忙,如果还是不够用就拒绝服务。
执行时不断从任务队列中获取任务进行执行。
1.Task任务提交
从类图上可以看到,接口ExecutorService继承自Executor,抽象类AbstractExecutorService类实现了ExecutorService接口,
ThreadPoolExecutor又继承了
AbstractExecutorService类。
(1) 看AbstractExecutorService中代码提交部分:
public <T> Future<T>submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask =newTaskFor(task);//将Callable类型的参数封装成FutureTask
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T>newTaskFor(Callable<T> callable) {
return newFutureTask<T>(callable);
}
构造好一个FutureTask对象后,调用execute()方法执行任务。这个方法是顶级接口Executor中定义的最重要的方法。
从上面看出,FutureTask既能被执行,又能被返回,是因为:
- FutureTask类型实现了Runnable接口,因此满足Executor中execute()方法的约定。
- FutureTask类型同时又实现了Future接口,该对象在execute执行后,就又作为submit方法的返回值返回,因为FutureTask实现了Future接口,满足Future接口的约定。
观察下子类
ThreadPoolExecutor ,创建线程池实际是生成该类的一个对象:
先补充下ThreadPoolExecutor的两个最重要的数据结构,分别是存储接收任务的任务队列和用来干活的作业集合。
//任务队列(阻塞队列,参看之前关于它的笔记)
private final BlockingQueue<Runnable> workQueue;
//作业线程集合
private final HashSet<Worker> workers = new HashSet<Worker>();
简单梳理下思路:
构建线程池时定义了一个额定大小,当线程池内工作线程数小于额定大小,有新任务进来就创建新工作线程,如果超过该阈值,把接收任务加到任务队列里面。但是如果任务队列里的任务实在太多了,那还是要申请额外的工作线程来帮忙。如果还是不够用就拒绝服务。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {//第一个条件满足,||后面的将不再执行
if (runState == RUNNING && workQueue.offer(command)) {//线程数超过阈值,不再创建线程,将任务加入阻塞队列
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
else if (!addIfUnderMaximumPoolSize(command))
reject(command); // is shutdown or saturated
}
}
addIfUnderCorePoolSize方法检查如果当前线程池的大小小于配置的核心线程数,说明还可以创建新线程,则启动新的线程执行这个任务。
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
//如果当前线程池的大小小于配置的核心线程数,说明还可以创建新线程,就为该任务创建一个线程去处理
if (poolSize < corePoolSize && runState == RUNNING)
// 则启动新的线程执行这个任务
t = addThread(firstTask);
return t != null;
}
调用adThread方法创建一个工作线程,差别是创建的有些工作线程上面关联接收到的任务firstTask,有些没有。该方法为当前接收到的任务firstTask创建Worker,并将Worker添加到作业集合HashSet<Worker> workers中,并启动作业。
private ThreadaddThread(Runnable firstTask) {
Worker w = new Worker(firstTask);//为当前接收到的任务firstTask创建Worker,即将task和worker绑定
Thread t = threadFactory.newThread(w);//将该作业worker绑定一个线程
w.thread = t;
//将Worker添加到作业集合HashSet<Worker> workers中,并启动作业
workers.add(w);
t.start();
return t;
}
至此,任务提交过程简单描述完毕。
2.任务执行
上面作业线程启动start之后,就要执行Worker的run方法,看作业线程干什么当然是看它的run方法在干什么。如我们所料,作业线程就是在一直调用getTask方法获取任务,然后调用 runTask(task)方法执行任务。在while循环里面,就是不干完不罢休的意思!
public voidrun() {
try {
Runnable task = firstTask;
//循环从线程池的任务队列workQueue获取任务
while (task != null || (task =getTask()) != null) {
//执行任务
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
然后简单看下Worker的两个方法getTask和runTask(task)方法的内容。
getTask方法是ThreadPoolExecutor提供给其内部类Worker的的方法。getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
Runnable getTask() {
for (;;) {
//从任务队列的头部取任务
r = workQueue.take();
return r;
}
}
runTask(Runnable task)是工作线程Worker真正处理拿到的每个具体任务。
private void runTask(Runnable task) {
//调用任务的run方法,即在Worker线程中执行Task内定义内容。
task.run();
}
需要注意的地方出现了,调用的其实是
FutureTask
task的run方法。FutureTask在线程池中等待被执行,最终执行的是FutureTask的run方法。看下FutureTask的run方法做了什么事情。
FutureTask即有Runnable接口的run方法来定义任务内容,也有Future接口中定义的get、cancel等方法来控制任务执行和获取执行结果。
Runnable接口自不用说,Future接口的伟大设计,就是使得实现该接口的对象可以阻塞线程直到任务执行完毕,也可以取消任务执行,检测任务是执行完毕还是被取消了。想想在之前我们使用Thread.join()或者Thread.join(long millis)等待任务结束是多么苦涩啊。
和其他的同步工具类一样,FutureTask的主要工作内容也是委托给其定义的内部类Sync来完成。
public void run() {
//调用Sync的对应方法
sync.innerRun();
}
FutureTask.Sync.innerRun(),这样做的目的就是为了维护任务执行的状态,只有当执行完后才能够获得任务执行结果。在该方法中,首先设置执行状态为RUNNING只有判断任务的状态是运行状态,才调用任务内封装的回调,并且在执行完成后设置回调的返回值到FutureTask的result变量上。
注意:FutureTask.run方法是在线程池中被执行的,而非主线程
1、通过执行Callable任务的call方法;
2、如果call执行成功,则通过set方法保存结果;
3、如果call执行有异常,则通过setException保存异常;
void innerRun() {
//通过对AQS的状态位state的判断来判断任务的状态是运行状态,则调用任务内封装的回调,并且设置回调的返回值
if (getState() == RUNNING)
innerSet(callable.call());
}
void innerSet(V v) {
for (;;) {
int s = getState();
//设置运行状态为完成,并且把回调额执行结果设置给result变量
if (compareAndSetState(s, RAN)) {
result = v;
releaseShared(0);
done();
return;
}
}
至此工作线程执行Task就结束了。提交的任务是由Worker工作线程执行,正是在该线程上调用Task中定义的任务内容,即封装的Callable回调,并设置执行结果。
3.获取执行结果
获取执行结果这个过程总是最容易的事情,只需调用FutureTask的get()方法即可。该方法是在Future接口中就定义的。get方法的作用就是等待执行结果。
FutureTask的get方法同样委托给Sync来执行,依靠内部类Sync完成同步。
public V get() throws InterruptedException, ExecutionException {
return sync.innerGet();
}
V innerGet() throws InterruptedException, ExecutionException {
//获得锁,表示执行完毕,才能获得后执行结果,否则阻塞等待执行完成再获取执行结果
acquireSharedInterruptibly(0);
return result;
}
protected int tryAcquireShared(int ignore) {
return innerIsDone()? 1 : -1;
}
如果执行完成了则可以继续执行后面的代码,返回result结果,否则如果还未完成,则阻塞线程等待执行完成,谁调用get谁阻塞。调用FutureTask的get方法的线程,一般应该就是提交Task的线程,在例子中就是main线程,main调用get会被阻塞,main中get后面的代码System.out.println(outputs);
会等结果返回后再执行。
四.总结
1.外面需要提交任务的角色(如例子中老大的老大),首先创建一个任务执行服务ExecutorService,一般使用工具类Executors的若干个工厂方法 创建不同特征的线程池ThreadPoolExecutor,例子中是使用newFixedThreadPool方法创建有n个固定工作线程的线程池。
2.线程池是专门负责从外面接活的老大。把任务封装成一个FutureTask对象,并根据输入定义好要获得结果的类型,就可以submit任务了。
3.线程池就像我们团队里管人管项目的老大,各个都有一套娴熟、有效的办法来对付输入的任务和手下干活的兄弟一样,内部有一套比较完整、细致的任务管理办法,工作线程管理办法,以便应付输入的任务。这些逻辑全部在其execute方法中体现。
4.线程池接收输入的task,根据需要创建工作线程,启动工作线程来执行task。
5.工作线程在其run方法中一直循环,从任务队列领取可以执行的task,调用task的run方法执行task内定义的任务。
6.FutureTask的run方法中调用其内部类Sync的innerRun方法来执行封装的具体任务,并把任务的执行结果返回给FutureTask的result变量。
7.当提及任务的角色调用FutureTask的get方法获取执行结果时,Sync的innerGet方法被调用。根据任务的执行状态判断,任务执行完毕则返回执行结果;未执行完毕则等待。