前言
官方文档中Executor是一个将任务Runnable的提交与执行进行分离的对象。Executor接口中唯一入口是execute方法,类似于Runnable的run方法。在实际的应用系统中,Runnable作为Thread的传入参数(或者运行对象),由Thread的start方法进行启动。程序在线程中进行调用的入口是Runnable的run,那么为什么又需要一个Executor来执行Runnable?通过new Threa(Runnable).start()不就可以实现任务的线程调用吗?Runnable提交与分离分别又是指代什么内容呢?Executor又能用于什么场景的编程环境?这些问题都是本篇文章的写作目的。
JVM内存模型及线程
在当代计算系统中,程序执行的最小单位是线程Thread。进程Process是应用程序的单位,在进程中同时开启多个线程Thread用于相互独立的任务Task(抽象、离散的工作单位)的并行计算,从而提高物理CPU的利用率,程序获得良好的吞吐量和快速的响应性。如果在应用程序中对于每个任务Task都开启一个线程Thread来执行,那么是否这个应用序就能够高效运行呢?线程在创建的过程中是需要消耗系统资源,也就是CPU、内存。当前计算系统的CPU的计算频率有大小限制,当启动的线程太多,容易造成CPU的超负荷工作,从而导致系统宕机或者拒绝执行风险。抛开操作系统中进程、线程以及调度问题。在JVM的内存模型,每个线程都具有自己的虚拟机栈VM Stack、本地方法栈native Stack和程序计数器,每创建一个线程,都需要耗费资源为这些栈内存开辟空间,同时还有Thread对象、Runnable对象在堆中的内存以及系统CPU创建的内核线程的调用、CPU线程调度等,如下图。那么,线程并行计算所带来的性能提升和线程创建、协同与调度带来的性能消耗是并行程序性能评价的关键因素之一。
线程池
在程序开发过程中,应用程序在一定范围内增加线程可以提高系统的吞吐量,但是一旦超出了这个范围,再创建更多的线程则会拖垮程序。例如,WebServer服务器响应用户请求,在并发量比较小的情况下,服务器完全可以响应用户的请求,但是当并发量达到一定的数量后,服务器响应出现响应缓慢、异常或宕机等问题。一个用户请求结束后,处理这个请求的线程则等待JVM的垃圾收集器GC进行回收,当下一次用户重新请求时,又创建一个全新的线程进行处理,如此往复。在服务端的内存中则贮存了许多使用过的线程对象,而JVM的GC回收内存的时间又不确定,则容易造成server端内存消耗。考虑这样一种思路,限制服务端线程的创建个数,同时将这些创建线程进行再利用,也就是处理完一个用户请求后进行回收利用,用于下一个用户请求。例如,公交车运行一样,限制载客数量,当有乘客上车时到达目的地,乘客下车,同时如果有另一个乘客上车,那么又可以乘坐。在SSH框架开发Web应用程序时,在Hibernate配置文档中往往需要配置缓存,在Spring中配置DataSource都是相同的原因。
Executor框架
Executor的核心是将任务Task的提交与实现分离,这句话是什么意思呢?在下面的程序中说明了提交和执行分离的思想。Executor只是一个框架的顶端接口,在抽象层代表的是一个执行器。那么对于执行器而言,它需要具备哪些功能、属性和状态,所以出现了它的实现类、子接口等内容,如下图。
public class ExecutorTest {
public static void main(String[] args) {
Executor executor = new Executor() {
public void execute(Runnable command) {
//在execute进行具体执行,例如同步执行,还是异步执行
command.run(); //同步执行
new Thread(command).start(); //异步执行
}
};
//通过execute进行了提交
executor.execute(new Runnable() {
public void run() {
for(int i = 1 ; i <= 100; i++) {
System.out.println("Thread" + Thread.currentThread().getId() + " : " + i);
}
}
});
}
}
ExecutorService框架生命周期管理
Executor框架在进行任务Task的处理过程中涉及几个执行器的状态:运行、关闭、终止。运行状态是Executor执行任务状态,对于不同实现方式的Executor内部执行可能不同。关闭状态是Executor停止对任务的执行,这种停止可以是平缓,也可以是粗暴的方式,为此ExecutorService定义了两种方法:shutdown(启动一次顺序关闭,执行以前提交的任务,但不接受新任务),shutdownNow(试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表)。终止状态是请求关闭后所有任务都完成后的状态。常用网络请求来作为例子进行讲解,如下代码所示。ExecutorService的所有API如下图所示。
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class ExecutorServiceTest {
public static void main(String[] args) {
try {
new Thread(new NetworkService(10010, 10)).start();
} catch(Exception e) {
e.printStackTrace();
}
}
}
class NetworkService implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(int port, int poolSize) throws IOException{
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
}
public void run() {
try {
while(true) {
// 监听服务器是否有用户请求
pool.execute(new Handler(serverSocket.accept()));
}
} catch(IOException ex) {
ex.printStackTrace();
pool.shutdown(); //当出现因为文件读写而抛出的异常时,关闭Executor;
//此时Executor的线程池中的任务是拒接接受新的任务提交,正在执行的继续执行,等待执行的等待执行完成
//直到所有的任务都完成了,则Executor终止
}
}
}
class Handler implements Runnable {
private final Socket socket;
Handler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
Reader reader = new InputStreamReader(socket.getInputStream());
int ch = reader.read();
while(ch >= 0) {
System.out.print((char)ch);
ch = reader.read();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
AbstractExecutorService默认实现
ExecutorService是一个接口,它和Executor本身都带着API,由AbstractExecutorService提供部分API的默认实现方式,其中核心的API为submit、invokeAny和invokeAll。下图给出了它的实现关系。
- submit方法__提交任务
/**
* 通过提交Runnable到Executor中用于执行,同时返回该Runnable所对应的Future
* 该Future是Runnbale在执行过程中的状态对象,而不是Executor的状态对象
*/
public Future<?> submit(Runnable task) {
//不能将null提交,进行空检查
if (task == null) throw new NullPointerException();
FutureTask<Object> ftask = new FutureTask<Object>(task, null);
execute(ftask);
return ftask;
}
/**
* 通过提交Runnable到Executor中用于执行,同时返回该Runnable所对应的Future
* 该Future是Runnbale在执行过程中的状态对象,而不是Executor的状态对象
* 当Runnable成功完成时则返回result
*/
public <T> Future<T> submit(Runnable task, T result) {
//不能将null提交,进行空检查
if (task == null) throw new NullPointerException();
FutureTask<T> ftask = new FutureTask<T>(task, result);
execute(ftask);
return ftask;
}
/**
* 通过提交Callable到Executor中用于执行,同时返回该Callable所对应的Future
* 该Future是Callable在执行过程中的状态对象,而不是Executor的状态对象
*/
public <T> Future<T> submit(Callable<T> task) {
//不能将null提交,进行空检查
if (task == null) throw new NullPointerException();
FutureTask<T> ftask = new FutureTask<T>(task);
execute(ftask);
return ftask;
}
- invokeAny 执行给定的任务
/**
* 如果某个任务已成功完成,则返回其结果,退出Executor
*/
public <T> T invokeAny(Collection<Callable<T>> tasks) throws
InterruptedException, ExecutionException {
//内部调用doInvokeAny方法来实现
... ...
}
/**
* 如果在给定的超时期满前某个任务已成功完成,则返回其结果
*/
public <T> T invokeAny(Collection<Callable<T>> tasks, long timeout,
TimeUnit unit) throws InterruptedException, ExecutionException,
TimeoutException {
//内部调用doInvokeAny方法来实现
... ...
}
/**
* the main mechanics of invokeAny.
*/
private <T> T doInvokeAny(Collection<Callable<T>> tasks,
boolean timed, long nanos) throws InterruptedException,
ExecutionException, TimeoutException {
if (tasks == null) //非null判断
throw new NullPointerException();
int ntasks = tasks.size();
if (ntasks == 0) //集合非空判断
throw new IllegalArgumentException();
//每一个Callable对应一个Future
List<Future<T>> futures= new ArrayList<Future<T>>(ntasks);
/**
* this指代当前Executor对象,执行任务
*/
ExecutorCompletionService<T> ecs = new ExecutorCompletionService<T>(this);
/**
* 为了效率考虑,尤其是并行限制情况下,对提交的任务进行检查,是否已经提交过。因
* 为重复提交相同的任务和异常机制会导致mainloop的混乱。
*/
try {
// ee用于保存异常,如果未能够获取任何结果则抛出最后获得的异常
ExecutionException ee = null;
long lastTime = (timed)? System.nanoTime() : 0;
Iterator<Callable<T>> it = tasks.iterator();
// 取任务集合中的第一个任务
futures.add(ecs.submit(it.next()));
--ntasks; //将当前任务个数减一,记录已经有多少个任务被提交了
int active = 1; //已经提交了第一个任务,active用于记录提交了几个任务并且在执行
/**
* 轮询当前Executor是否已经有任务完成了,一旦完成则通过Future.get获取结
* 果,如果在get过程中抛出异常,那么继续轮询Executor。此外,当Executor中
* 的线程队列中已经没有任务可以执行,同时tasks中还有任务待执行,则提交任务
* 到Executor中进行执行,并且进行轮询Executor获取结果。
*/
for (;;) {
Future<T> f = ecs.poll(); //获取已经提交的任务的执行结果
if (f == null) { //如果为空,分成四种情况进行处理
if (ntasks > 0) { // 首先,从任务集合tasks中取出一个任务提交Executor执行
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
} else if (active == 0)//其次,任务集合task已经没有任务了,此时也没有任务在Executor中执行了,那么退出轮询
break;
else if (timed) {//其次,如果设置了时间限制,那么进行时间比较
//Executor等待预设的时间nanos来获取任务执行结果Future
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)//等待时间过去仍然没有获取到Future,则抛出超时异常
throw new TimeoutException();
/**
* 这个步骤是取系统时间来更新等待时间,也就是逐渐缩短等待时间
*/
long now = System.nanoTime();
nanos -= now - lastTime;
lastTime = now;
} else //最后,如果任务集合没有任务,当前Executor还有任务没执行完成,而timed没有进行等待时间设置,那么进行等待执行结果
f = ecs.take();
}
if (f != null) { //如果获取到了任务的执行结果Future
--active; //已经有一个任务执行完成,则减一
try {
return f.get(); //返回当前任务的Future的结果
/**
* 查看当前任务是否是因为异常而完成,如果是则记录当前异常,继续轮询获取结果
*/
} catch(InterruptedException ie) {
throw ie;
} catch(ExecutionException eex) {
ee = eex;
} catch(RuntimeException rex) {
ee = new ExecutionException(rex);
}
}
}
//如果最后仍然没有获取到结果,而ee为null,则抛出执行异常
if (ee == null)
ee = new ExecutionException();
throw ee;
} finally {
//关闭任务,释放资源
for (Future<T> f : futures)
f.cancel(true);
}
}
- invokeAll 执行给定的任务集合
/**
* 当所有任务完成时,返回保持任务状态和结果的 Future 列表,退出Executor
*/
public <T> List<Future<T>> invokeAll(Collection<Callable<T>> tasks,
long timeout, TimeUnit unit) throws InterruptedException {
//判断非null,抛出空指针异常
if (tasks == null || unit == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);
//预设一个task相关的结果Future集合
List<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false; //完成标识
try {
//将任务task集合和结果Futrue集合关联到FutureTask,以便以后Executor提交执行
for (Callable<T> t : tasks)
futures.add(new FutureTask<T>(t));
long lastTime = System.nanoTime();
//防止Executor没有足够的并行资源,提交检查、调用的交错检查
Iterator<Future<T>> it = futures.iterator();
//首先将所有的任务提交给Executor执行
while (it.hasNext()) {
execute((Runnable)(it.next())); //因为execute需要的是Runnable
long now = System.nanoTime();
nanos -= now - lastTime;
lastTime = now;
if (nanos <= 0) //如果超时,则返回
return futures;
}
//轮询当前futures集合,确定是否已经全部完成
for (Future<T> f : futures) {
if (!f.isDone()) { //如果还未完成
if (nanos <= 0) //如果已经超时
return futures;
try { //未到规定时间,则等待nanos时间进行获取
f.get(nanos, TimeUnit.NANOSECONDS);
} catch(CancellationException ignore) {
} catch(ExecutionException ignore) {
} catch(TimeoutException toe) {
return futures;
}
//缩短等待时间
long now = System.nanoTime();
nanos -= now - lastTime;
lastTime = now;
}
}
done = true;
return futures;
} finally {
if (!done) //如果是异常、超时等情况引起的方法结束,那么将所以Future进行取消,从而释放资源
for (Future<T> f : futures)
f.cancel(true);
}
}
invokeAny和invokeAll都是对提交的Collection任务集合进行提交执行。在它们的源码中可以看到,invokeAny是采用了及时获取原则;invokeAll则是一次提交,全部获取。