Java基础系列--Executor框架(一)
原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/8393618.html
一、Executor框架介绍
Executor框架是JDK1.5之后出现的,位于juc包中,是并发程序设计的工具之一。各个版本以来一直在进行修正。
Executor是执行者之意,表示任务的执行者,这里的任务指的是新的线程任务(实现Runnable接口的执行任务)。
整个Executor执行者框架包括多个接口和类,甚至还涉及到阻塞队列的使用,协同实现任务的执行。
下面是简单的Executor框架的类结构:

从上面的类结构中我们可以看到Executor接口是整个框架的祖接口,它大致规划了框架的结构,并定义了执行方法execute(),这个方法需要一个Runnable作为入参,表示执行一个线程任务。从这里也可以看出来这个框架的主要思想:将要执行的任务和具体的执行进行解耦,任务的内容单独定义为一个线程,任务的执行交给该框架进行,只需要将任务提交给框架即可(这个后面会提到)。Runnable入参就表示定义为单独线程的任务内容,execute方法则是执行任务,整个框架定义的就是这样一个任务执行器,Executor框架总的来说就是一个多线程任务执行框架。
二、Executor接口
Executor接口是整个框架的总接口,正如上面所述,它描述了框架的主要实现思想:任务内容与执行的解耦。其源码很短,我们可以看看:
package java.util.concurrent;
public interface Executor { /**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
这是一个单独的接口,其内部只有一个execute()方法。我们看看其注释:在将来某一时刻执行给定的指令,可能会在一个新的线程、或一个线程池中的线程、或在正调用的线程中执行,这取决于Executor接口的具体实现。
注意:这个方法中的入参任务指令是必不可少的,不可传null,否则会报NullPointerException(空指针异常)。
三、ExecutorService接口
ExecutorService接口继承了Executor接口,Executor接口仅仅描述了思想,定义了一个执行器,ExecutorService接口在其基础上进一步丰富了框架的接口,为框架定义了更多内容,包括:任务的提交,执行器的终止关闭等。
3.1 终止方法
void shutdown();
List<Runnable> shutdownNow();
如上源码,ExecutorService中定义了两个终止方法,这两个方法并不完全相同,第一个方法shutDown()的作用是终止新任务的接收,已接收的任务却需要继续执行。这是保证已提交任务全部执行的终止方法。第二个shutDownNow()方法属于强效终止方法,它会试图停止正在执行的线程任务,并且不再执行处于等待状态的其他任务,并且会将这些任务以列表的方式返回。
注意:第二个方法的试图停止,并不一定会停止,因为其实现会使用Thread.interrupt()方法来进行线程任务中断执行,但是如果任务线程不会响应该中断,则不会被终止。
3.2 任务提交方法
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
这三个任务提交方法采用方法重载的方式定义,其实均是对execute方法的再封装,对其进行扩展而来。因为execute方法只能接受Runnable入参,切无返回值。submit提交任务却拥有返回值,而且可以接收两种格式的任务,Callable和Runnable两种。不同的方法参数和返回值也略有不同。
第一种方法接收一个Callable入参,任务执行成功会返回一个表示该任务的Future,通过其get方法可获取到在Callable任务中指定的返回内容。
第二种方法接收一个Runnable入参和一个指定的返回值,任务执行成功会返回一个表示该任务的Future,通过其get方法可以获取到之前的入参result的值,即入参result即为预设的返回值。
第三种方法接收一个Runnable入参,任务执行成功会返回一个表示该任务的Future,通过get方法可得到null。
我们通过下面的实例来进行验证:
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
//第一种方法:入参为Callable
Future<String> result1 = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "task2";//此处的task2即为返回的内容,即future.get()的值
}
});
//第二种方法:入参为Runnable和T
Future<String> result2 = executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("mmp");
}
},"task1");//此处的task1即为返回的内容,即future.get()的值
//第三种方法:入参为Runnable
Future<?> result3 = executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("nnd");
}
});
System.out.println(result1.get());
System.out.println(result2.get());
System.out.println(result3.get());
}
执行结果为:
task2
mmp
task1
nnd
null
从上面的结果中也可以看出三个方法的不同之处。
3.3 invokeAny方法
<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;
第一种方法表示执行给定的任务列表中的任务,如果某个任务成功完成,没有任何异常,则将该任务的结果返回,一旦成功或者异常被返回之后,任务列表中其他任务则取消执行,那么可以看出返回的必定是第一个执行成功的任务的结果或者最后一个任务的执行异常。
第二个方法是在第一个方法的基础上加上一个超时限制,如果在超时期满之前完成了某个任务则返回该任务的结果,其余同上。
注意:这里写到一旦成功或者异常被返回,其实这里如果第一个任务执行的时候出现了异常,则同样会被返回,同样其他任务取消执行。
例子:
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
List<Callable<String>> callables = new ArrayList<>();
callables.add(new Callable<String>() {
@Override
public String call() throws Exception {
return "task1";
}
});
callables.add(new Callable<String>() {
@Override
public String call() throws Exception {
return "task2";
}
});
callables.add(new Callable<String>() {
@Override
public String call() throws Exception {
return "task3";
}
});
callables.add(new Callable<String>() {
@Override
public String call() throws Exception {
return "task4";
}
});
String s = executor.invokeAny(callables);
System.out.println(s);
}
执行结果:
task1
3.4 invokeAll方法
<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;
这两个方法和3.3的两个方法结构类似,第一个方法表示执行给定的任务列表中的任务,当列表中的所有任务执行完毕之后,返回所有任务的结果组成的列表,此时列表中所有的Future中的isDone均为true,表示所有任务均被执行。
第二个方法同样在第一个方法的基础上加上了时限限制,表示在所有任务完成或者时限到期之后将所有任务的结果组成列表返回,此时列表中所有的Future中的isDone均为true
注意:如果时限期满导致返回结果的话,那些未执行的任务的结果中是null,而isDone仍然为true,状态为6-INTERRUPTED(中断),而执行成功的任务的结果状态为2-COMPLETING(完成)
例子:
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
List<Callable<String>> callables = new ArrayList<>();
callables.add(new Callable() {
@Override
public Object call() throws Exception {
return "task1";
}
});
callables.add(new Callable() {
@Override
public Object call() throws Exception {
return "task2";
}
});
callables.add(new Callable() {
@Override
public Object call() throws Exception {
return "task3";
}
});
List<Future<String>> futures = executor.invokeAll(callables,1900L,TimeUnit.MICROSECONDS);
for(Future<String> future:futures){
System.out.println(future.get());
}
}
测试第一个方法就将22行第二个和第三个参数去掉即可,这里设置1900毫秒在我的电脑上正好能有几率测试到完成一部分就超时的情况,其执行结果为:
task1
task2
at java.util.concurrent.FutureTask.report(FutureTask.java:121)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at xxxTest.main(xxxTest.java:135)
其返回结果为:

全部成功的结果为:
task1
task2
task3
结果为:

四、AbstractExecutorService
这是一个抽象类,实现了ExecutorService接口。这是ExecutorService的默认实现,我们来看下AbstractExecutorService中实现的方法:
4.1 newTaskFor方法
可以从中看出,AbstractExecutorService实现了之前我们介绍的ExecutorService中的大部分方法,包括三个任务提交方法,两个invokeAny和两个invokeAll方法,其中doInvokeAny方法是私有方法,被invokeAny调用,只是多出了两个newTaskFor方法。
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);
}
可以看出,这两个newTaskFor是使用给定的参数组建一个FutureTask实例并返回。所以它的作用就是提供任务执行结果Future,只是这里提供的是FutureTask类型的Future,如果我们需要使用别的RunnableFuture的实现类型(FutureTask就是RunnableFuture的实现之一),我们可以自定义。
这两个方法被submit方法所调用,用于在任务执行之前,将其包装起来,然后调用execute执行即可,之前我们看过,execute的入参是Runnable类型,此处FutureTask的超接口RunnableFuture就实现了Runnable接口。所以可以直接将包装过的任务直接作为execute的入参进行执行。
4.2 submit提交方法
下面我们就来看看三个submit方法:
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;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
参考之前ExecuteService中的介绍和实例,我们可以轻松理解这里代码的含义,首先判断任务是否为null,若是null,则抛出空指针,否则使用newTaskFor将任务(和返回值)封装成为FutureTask,再将其作为入参调用execute进行任务执行。最后将之前封装的FutureTask作为返回值返回。
4.3 invokeAny方法
这里实现了invokeAny,核心是doInvokeAny方法,我们可以看下源码:
/**
* the main mechanics of invokeAny.
*/
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
if (tasks == null)
throw new NullPointerException();
int ntasks = tasks.size();
if (ntasks == 0)
throw new IllegalArgumentException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
ExecutorCompletionService<T> ecs =
new ExecutorCompletionService<T>(this); // For efficiency, especially in executors with limited
// parallelism, check to see if previously submitted tasks are
// done before submitting more of them. This interleaving
// plus the exception mechanics account for messiness of main
// loop. try {
// Record exceptions so that if we fail to obtain any
// result, we can throw the last exception we got.
ExecutionException ee = null;
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Iterator<? extends Callable<T>> it = tasks.iterator(); // Start one task for sure; the rest incrementally
futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1; for (;;) {
Future<T> f = ecs.poll();
if (f == null) {
if (ntasks > 0) {
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
}
else if (active == 0)
break;
else if (timed) {
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
else
f = ecs.take();
}
if (f != null) {
--active;
try {
return f.get();
} catch (ExecutionException eex) {
ee = eex;
} catch (RuntimeException rex) {
ee = new ExecutionException(rex);
}
}
} if (ee == null)
ee = new ExecutionException();
throw ee; } finally {
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}
解析:
1.参数校验,主要是看任务列表是否存在任务
2.优先执行一个任务,这个任务为任务列表tasks中的首个任务(30行),然后进入一个无限循环(当然会有退出条件)。
3.从执行器Executor的阻塞队列中移除队头的元素,并将该元素返回,如果队列为空队列,则这里返回值为null,此时会将优先提交的任务元素返回(35行),然后执行第53行。
4.执行器执行首个任务,执行56行,等待任务执行完成,如果任务执行成功,会在此处直接退出整个方法,一旦该任务执行出错,则会产生异常,并将异常保存在ee中(58行,60行),然后继续进行循环。
5.再次执行35行代码发现返回值为null,则会判断任务列表中的未执行任务数ntasks(该值初始为任务列表总任务数,但会随着任务的提交执行而逐渐递减,它的值就是任务列表中未提交执行的任务的数量)是否为0,此时该值一定不为0,则会执行38-40行代码,再次提交一个执行任务,然后会再次下一次循环,这次循环类似第4点。
6.一旦某个任务执行成功,就会将该任务的执行结果返回,但是一旦某个任务执行失败,则继续执行下个任务,如果所有任务都执行失败,则会将最后一个任务的失败异常抛出(这个异常将保存在ee中,一直到循环结束,由67行抛出)。
7.最后取消其他正在执行的任务(70-71行)。
总结:该方法会返回任务列表中第一个执行成功的任务的执行结果或者是抛出异常,一旦抛出了异常,表示任务全部被执行,但是全部失败。一旦某个任务执行成功,则剩下的任务将不会再执行,而且会取消其他正在执行的任务。
注意:对于有超时限制的情况,会执行44-49行代码,每个任务执行时都会进行超时判断,一旦超时期满,则抛出超时异常,并在最后取消所有正在执行的任务(70-71行)。
4.4 invokeAll方法
AbstractExecutorService中的两个invokeAll是分开实现的。如前所述,该方法用于执行一个任务列表,确保所有任务全部执行,也即All之意。
这里就先看到这里,更多内容下篇再写吧。
Java基础系列--Executor框架(一)的更多相关文章
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- Java 并发编程——Executor框架和线程池原理
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- 夯实Java基础系列11:深入理解Java中的回调机制
目录 模块间的调用 多线程中的"回调" Java回调机制实战 实例一 : 同步调用 实例二:由浅入深 实例三:Tom做题 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 ...
- 【Java 并发】Executor框架机制与线程池配置使用
[Java 并发]Executor框架机制与线程池配置使用 一,Executor框架Executor框架便是Java 5中引入的,其内部使用了线程池机制,在java.util.cocurrent 包下 ...
- 夯实Java基础系列1:Java面向对象三大特性(基础篇)
本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 [https://github.com/h2pl/Java-Tutorial](https: ...
- 夯实Java基础系列3:一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!
目录 目录 string基础 Java String 类 创建字符串 StringDemo.java 文件代码: String基本用法 创建String对象的常用方法 String中常用的方法,用法如 ...
- 夯实Java基础系列4:一文了解final关键字的特性、使用方法,以及实现原理
目录 final使用 final变量 final修饰基本数据类型变量和引用 final类 final关键字的知识点 final关键字的最佳实践 final的用法 关于空白final final内存分配 ...
- 夯实Java基础系列5:Java文件和Java包结构
目录 Java中的包概念 包的作用 package 的目录结构 设置 CLASSPATH 系统变量 常用jar包 java软件包的类型 dt.jar rt.jar *.java文件的奥秘 *.Java ...
- 夯实Java基础系列6:一文搞懂抽象类和接口,从基础到面试题,揭秘其本质区别!
目录 抽象类介绍 为什么要用抽象类 一个抽象类小故事 一个抽象类小游戏 接口介绍 接口与类相似点: 接口与类的区别: 接口特性 抽象类和接口的区别 接口的使用: 接口最佳实践:设计模式中的工厂模式 接 ...
随机推荐
- CSS基础知识(颜色、伪类、盒子模型)
6.设置颜色单位 L 普通英文单词 {color : 属性值red;} 此方法简单,便捷.但设置的颜色在不同浏览器中,可能显示的颜色出现差异 * 三原色 - 红.绿.蓝 L 颜色的八进制方式 ...
- ascii、unicode、utf-8、gbk区别及转换
一.编码 ascii: A:00000010 8位 一个字节 unicode: A:00000000 00000001 00000010 00000100 32位 四个字节 中:00000000 00 ...
- asp.net core 配置
ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML配置文件web.config,现在支持各种格式的配置,比以前灵活 ...
- 我的第六个网页制作:table标签
<!doctype html> <html> <head> <meta charset="utf-8"> <title> ...
- noi 2016 游记
先挖个坑..这回大概不会太监吧(大雾 day -2 下午起飞的飞机,晚上到了成都..把东西扔到旅馆后就组队外出觅食了... 街上人不多,逛了半天才发现一家卖本地小吃的小店. KPM:诶诶给我来碗酸辣粉 ...
- android仿漫画源码、抽奖转盘、Google相册、动画源码等
Android精选源码 android实现仿今日头条的开源项目 波浪效果,实现流量的动态显示 美妆领域的app, 集成了摄像头取色, 朋友圈, 滤镜等 android仿漫画源码 android一个视差 ...
- 用.net中的SqlBulkCopy类批量复制数据 (转载)
在软件开发中,把数据从一个地方复制到另一个地方是一个普遍的应用. 在很多不同的场合都会执行这个操作,包括旧系统到新系统的移植,从不同的数据库备份数据和收集数据. .NET 2.0有一个SqlBulkC ...
- tomcat更改端口号
apache-tomcat-8文件下的conf文件下的server.xml文件打开将 <Connector port="8080" protocol="HTT ...
- linux服务器,svn认证失败,配置问题,防火墙等等
之前自己还真没设置过SVN,今天亲自动手,错误百出,真是够头疼的.在网上随便找了一篇文章,就按照文章介绍开始安装.怎么安装和设置我就不说了,这里主要记录遇到的问题. 1.不知道该怎么设置 svn:// ...
- Vuthink正确安装过程
1. 下载项目vuethink,本例将项目放置website文件下. 2. 后台搭建 本地建站–>以phpstudy为例 1) 新建站点域名 <Virtual ...