本文转载自:https://blog.youkuaiyun.com/u010425776/article/details/54580157
Executors框架简介
Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逸出。
Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。
Executor类
Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。
ExecutorService类
ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。
ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。
Executors类
Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
-
public static ExecutorService newFixedThreadPool(int nThreads)
- 创建固定数目线程的线程池。
- newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程;
- 任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子;
- 和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器;
- 从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
- fixed池线程数固定,并且是0秒IDLE(无IDLE)
- cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE。
-
public static ExecutorService newCachedThreadPool()
- 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
- 缓存型池子通常用于执行一些生存期很短的异步型任务;
- 注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。
- 缺省timeout是60s。
-
public static ExecutorService newSingleThreadExecutor()
- 创建一个单线程化的Executor。
- 用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)
-
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。Timer类存在以下缺陷:- Timer类不管启动多少定时器,但它只会启动一条线程,当有多个定时任务时,就会产生延迟。如:我们要求一个任务每隔3S执行,且执行大约需要10S,第二个任务每隔5S执行,两个任务同时启动。若使用Timer我们会发现,第而个任务是在第一个任务执行结束后的5S才开始执行。这就是多任务的延时问题。
- 若多个定时任务中有一个任务抛异常,那所有任务都无法执行。
- Timer执行周期任务时依赖系统时间。若系统时间发生变化,那Timer执行结果可能也会发生变化。而ScheduledExecutorService基于时间的延迟,并非时间,因此不会由于系统时间的改变发生执行变化。
综上所述,定时任务要使用ScheduledExecutorService取代Timer。
Executor执行任务
在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。
Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
Executor执行Runnable任务
通过Executors的以上四个静态工厂方法获得 ExecutorService实例,而后调用该实例的execute(Runnable command)方法即可。一旦Runnable任务传递到execute()方法,该方法便会自动在一个线程上执行。
// 获取ExecutorService实例
ExecutorService executorService = Executors.newCachedThreadPool();
// 提交任务
executorService.execute( new Runnable(){
public void run(){
//……
}
} );
Executor执行Callable任务
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 提交任务
Future<String> future = executorService.submit( new Callable<String>{
public String call(){
// ……
}
} );
// 获取执行结果
if ( future.isDone ) {
String result = future.get();
}
// 关闭线程池
executorService.shutdown();
- Callable表示call函数返回值为String类型;
- 如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。
ThreadPoolExecutor类
该类用于构造自定义的线程池。构造方法如下:
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)
- corePoolSize:线程池中所保存的核心线程数,包括空闲线程。线程池认为这是一个最合理的值,它会尽量使得线程数量维持在这个值上下。
- maximumPoolSize:池中允许的最大线程数。
- keepAliveTime:线程池中的空闲线程所能持续的最长时间。
- unit:持续时间的单位。
- workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。
当试图通过excute方法将一个Runnable任务添加到线程池中时,按照如下顺序来处理:
- 如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;
- 如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则不再创建新的线程,并将新任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);
- 如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;
- 如果线程池中的线程数量等于了maximumPoolSize,有4种才处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。
- 另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。
批量获取多条线程的执行结果
当向线程池提交callable任务后,我们可能需要一次性获取所有返回结果,有三种处理方法。
方法一:自己维护返回结果
// 创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 存储执行结果的List
List<Future<String>> results = new ArrayList<Future<String>>();
// 提交10个任务
for ( int i=0; i<10; i++ ) {
Future<String> result = executorService.submit( new Callable<String>(){
public String call(){
int sleepTime = new Random().nextInt(1000);
Thread.sleep(sleepTime);
return "线程"+i+"睡了"+sleepTime+"秒";
}
} );
// 将执行结果存入results中
results.add( result );
}
// 获取10个任务的返回结果
for ( int i=0; i<10; i++ ) {
// 获取包含返回结果的future对象
Future<String> future = results.get(i);
// 从future中取出执行结果(若尚未返回结果,则get方法被阻塞,直到结果被返回为止)
String result = future.get();
System.out.println(result);
}
此方法的弊端:
- 需要自己创建容器维护所有的返回结果,比较麻烦;
- 从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。
方法二:使用ExecutorService的invokeAll函数
本方法能解决第一个弊端,即并不需要自己去维护一个存储返回结果的容器。当我们需要获取线程池所有的返回结果时,只需调用invokeAll函数即可。
但是,这种方式需要你自己去维护一个用于存储任务的容器。
// 创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 创建存储任务的容器
List<Callable<String>> tasks = new ArrayList<Callable<String>>();
// 提交10个任务
for ( int i=0; i<10; i++ ) {
Callable<String> task = new Callable<String>(){
public String call(){
int sleepTime = new Random().nextInt(1000);
Thread.sleep(sleepTime);
return "线程"+i+"睡了"+sleepTime+"秒";
}
};
executorService.submit( task );
// 将task添加进任务队列
tasks.add( task );
}
// 获取10个任务的返回结果
List<Future<String>> results = executorService.invokeAll( tasks );
// 输出结果
for ( int i=0; i<10; i++ ) {
// 获取包含返回结果的future对象
Future<String> future = results.get(i);
// 从future中取出执行结果(若尚未返回结果,则get方法被阻塞,直到结果被返回为止)
String result = future.get();
System.out.println(result);
}
方法三:使用CompletionService
CompletionService内部维护了一个阻塞队列,只有执行完成的任务结果才会被放入该队列,这样就确保执行时间较短的任务率先被存入阻塞队列中。
ExecutorService exec = Executors.newFixedThreadPool(10);
final BlockingQueue<Future<Integer>> queue = new LinkedBlockingDeque<Future<Integer>>(
10);
//实例化CompletionService
final CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(
exec, queue);
// 提交10个任务
for ( int i=0; i<10; i++ ) {
exec.submit( new Callable<String>(){
public String call(){
int sleepTime = new Random().nextInt(1000);
Thread.sleep(sleepTime);
return "线程"+i+"睡了"+sleepTime+"秒";
}
} );
}
// 输出结果
for ( int i=0; i<10; i++ ) {
// 获取包含返回结果的future对象(若整个阻塞队列中还没有一条线程返回结果,那么调用take将会被阻塞,
//当然你可以调用poll,不会被阻塞,若没有结果会返回null,poll和take返回正确的结果后会将该结果从队列中删除)
Future<String> future = completionService.take();
// 从future中取出执行结果,这里存储的future已经拥有执行结果,get不会被阻塞
String result = future.get();
System.out.println(result);
}
线程池的作用
-
减少资源的开销
减少了每次创建线程、销毁线程的开销。 -
提高响应速度
每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度。 -
提高线程的可管理性
线程是一种稀缺资源,若不加以限制,不仅会占用大量资源,而且会影响系统的稳定性。
因此,线程池可以对线程的创建与停止、线程数量等等因素加以控制,使得线程在一种可控的范围内运行,不仅能保证系统稳定运行,而且方便性能调优。
线程池的实现原理
线程池一般由两种角色构成:多个工作线程 和 一个阻塞队列。
-
工作线程
工作线程是一组已经处在运行中的线程,它们不断地向阻塞队列中领取任务执行。 -
阻塞队列
阻塞队列用于存储工作线程来不及处理的任务。当工作线程都在执行任务时,到来的新任务就只能暂时在阻塞队列中存储。
ThreadPoolExecutor的使用
创建线程池
通过如下代码即可创建一个线程池:
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, runnableTaskQueue, handler);
- corePoolSize:基本线程数量
它表示你希望线程池达到的一个值。线程池会尽量把实际线程数量保持在这个值上下。
corePoolSize:基本线程数量
它表示你希望线程池达到的一个值。线程池会尽量把实际线程数量保持在这个值上下。
-
maximumPoolSize:最大线程数量
这是线程数量的上界。
如果实际线程数量达到这个值:- 阻塞队列未满:任务存入阻塞队列等待执行
- 阻塞队列已满:调用饱和策略
-
keepAliveTime:空闲线程的存活时间
当实际线程数量超过corePoolSize时,若线程空闲的时间超过该值,就会被停止。
PS:当任务很多,且任务执行时间很短的情况下,可以将该值调大,提高线程利用率。 -
timeUnit:keepAliveTime的单位
-
runnableTaskQueue:任务队列
这是一个存放任务的阻塞队列,可以有如下几种选择:-
ArrayBlockingQueue
它是一个由数组实现的阻塞队列,FIFO。 -
LinkedBlockingQueue
它是一个由链表实现的阻塞队列,FIFO。
吞吐量通常要高于ArrayBlockingQueue。fixedThreadPool使用的阻塞队列就是它。
它是一个无界队列。 -
SynchronousQueue
它是一个没有存储空间的阻塞队列,任务提交给它之后必须要交给一条工作线程处理;如果当前没有空闲的工作线程,则立即创建一条新的工作线程。
cachedThreadPool用的阻塞队列就是它。
它是一个无界队列。 -
PriorityBlockingQueue
它是一个优先权阻塞队列。
-
handler:饱和策略
当实际线程数达到maximumPoolSize,并且阻塞队列已满时,就会调用饱和策略。
JDK1.5由四种饱和策略:
- AbortPolicy
默认。直接抛异常。 - CallerRunsPolicy
只用调用者所在的线程执行任务。 - DiscardOldestPolicy
丢弃任务队列中最久的任务。 - DiscardPolicy
丢弃当前任务。
提交任务
可以向ThreadPoolExecutor提交两种任务:Callable和Runnable。
-
Callable
该类任务有返回结果,可以抛出异常。
通过submit函数提交,返回Future对象。
可通过get获取执行结果。 -
Runnable
该类任务只执行,无法获取返回结果,并在执行过程中无法抛异常。
通过execute提交。
关闭线程池
关闭线程池有两种方式:shutdown和shutdownNow,关闭时,会遍历所有的线程,调用它们的interrupt函数中断线程。但这两种方式对于正在执行的线程处理方式不同。
- shutdown()
仅停止阻塞队列中等待的线程,那些正在执行的线程就会让他们执行结束。 - shutdownNow()
不仅会停止阻塞队列中的线程,而且会停止正在执行的线程。
ThreadPoolExecutor运行机制
当有请求到来时:
- 若当前实际线程数量 少于 corePoolSize,即使有空闲线程,也会创建一个新的工作线程;
- 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行;
- 若当前实际线程数量 小于 maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务;
- 若当前实际线程数量已经达到maximumPoolSize,并且阻塞队列已满,则使用饱和策略。
设置合理的线程池大小
任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。
-
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。 -
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。
IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。 -
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
Executor两级调度模型
在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程。
在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执行它们;
在操作系统层面,操作系统再将这些线程分配给处理器执行。
Executor结构
Executor框架中的所有类可以分成三类:
-
任务
任务有两种类型:Runnable和Callable。 -
任务执行器
Executor框架最核心的接口是Executor,它表示任务的执行器。
Executor的子接口为ExecutorService。
ExecutorService有两大实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor。 -
执行结果
Future接口表示异步的执行结果,它的实现类为FutureTask。
线程池
Executors工厂类可以创建四种类型的线程池,通过Executors.newXXX即可创建。
1. FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- 它是一种固定大小的线程池;
- corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
- keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
- 阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;
- 由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
- 由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。
2. CachedThreadPool
public static ExecutorService newCachedThreadPool(){
return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
}
- 它是一个可以无限扩大的线程池;
- 它比较适合处理执行时间比较小的任务;
- corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
- keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
- 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
3. SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(){
return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
- 它只会创建一条工作线程处理任务;
- 采用的阻塞队列为LinkedBlockingQueue;
4. ScheduledThreadPool
它用来处理延时任务或定时任务。
-
它接收SchduledFutureTask类型的任务,有两种提交任务的方式:
- scheduledAtFixedRate
- scheduledWithFixedDelay
-
SchduledFutureTask接收的参数:
- time:任务开始的时间
- sequenceNumber:任务的序号
- period:任务执行的时间间隔
-
它采用DelayQueue存储等待的任务
- DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
- DelayQueue也是一个无界队列;
-
工作线程的执行过程:
-
工作线程会从DelayQueue取已经到期的任务去执行;
-
执行结束后重新设置任务的到期时间,再次放回DelayQueue。
-