10.1 Executor框架简介
10.1.1 Executor框架的两级调度模型
在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线
程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程
也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
10.1.2 Executor框架的结构与成员
本文将分两部分来介绍Executor:Executor的结构和Executor框架包含的成员组件。
1.Executor框架的结构
Executor框架主要由3大部分组成如下。·任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
·任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的
ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口
(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
·异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
Executor框架包含的主要的类与接口如图10-2所示。
下面是这些类和接口的简介。
·Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开
来。
·ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
·ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执
行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
·Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
·Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-
ThreadPoolExecutor执行。
Executor框架的使用示意图如图10-3所示。
图10-2 Executor框架的类与接口
主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一
个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或
Executors.callable(Runnable task,Object resule))。
然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable
command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(Executor-
Service.submit(Runnable task)或ExecutorService.submit(Callable<T>task))。
如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象
(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可
以创建FutureTask,然后直接交给ExecutorService执行。
最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
以上内容来源于《JAVA并发编程的艺术》一书
ThreadpoolExecutor详解
public ThreadPoolExecutor(int corePoolSize,//核心线程数量 int maximumPoolSize,//最大线程数量 long keepAliveTime,//超时时间,超过(maximumPoolSize-corePoolSize)线程存活的时间 TimeUnit unit,//存活时间单位 BlockingQueue<Runnable> workQueue), //存放任务的队列 ThreadFactory threadFactory,//创建 新线程使用的工厂 RejectedExecutionHandler handler) {//当任务不能执行时采用的处理方式
线程池初始化时是没有创建线程的,线程池里的线程的初始化与其他线程一样,但是在完成
任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线
程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造
成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开
销
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超
过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0意味着多余的线程会被立即终止,也就是超出核心
线程数量以外的线程空余存活时间
而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,
相当于没有上限
这个线程池执行任务的流程如下:
1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务2. 线程数等于核心线程数后,将任务加入阻塞队列
3. 由于队列容量非常大,可以一直添加
4. 执行完任务的线程反复去队列中取任务执行
用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量
FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为
Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中
的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或
shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空
闲线程,若无可回收,则新建线程; 并且没有核心线程,非核心线程数无上限,但是每个空闲
的时间只有 60 秒,超过后就会被回收。
它的执行流程如下:
1. 没有核心线程,直接向 SynchronousQueue 中提交任务
2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活
下去,否则就被回收
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定
顺序(FIFO, LIFO, 优先级)执行
以下部分来源于《JAVA并发编程的艺术》一书
ScheduledThreadPoolExecutor详解
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运
行任务,或者定期执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但
ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而
ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
ScheduledThreadPoolExecutor的执行主要分为两大部分。
1)当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWith-
FixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了
RunnableScheduledFutur接口的ScheduledFutureTask。
2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下
的修改。
·使用DelayQueue作为任务队列。
·获取任务的方式不同(后文会说明)。
·执行周期任务后,增加了额外的处理(后文会说明)。
10.3.2 ScheduledThreadPoolExecutor的实现
前面我们提到过,ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)
放到一个DelayQueue中。
ScheduledFutureTask主要包含3个成员变量,如下。
·long型成员变量time,表示这个任务将要被执行的具体时间。
·long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中
的序号。
·long型成员变量period,表示任务执行的间隔周期。
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-
FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个
ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就
是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。
首先,让我们看看ScheduledThreadPoolExecutor中的线程执行周期任务的过程。图10-9是
ScheduledThreadPoolExecutor中的线程1执行某个周期任务的4个步骤。
图10-9 ScheduledThreadPoolExecutor的任务执行步骤
下面是对这4个步骤的说明。
1)线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务
是指ScheduledFutureTask的time大于等于当前时间。
2)线程1执行这个ScheduledFutureTask。
3)线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。
4)线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(Delay-
Queue.add())。
接下来,让我们看看上面的步骤1)获取任务的过程。下面是DelayQueue.take()方法的源代码实现。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 1
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await(); // 2.1
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay); // 2.2
} else {
E x = q.poll(); // 2.3.1
assert x != null;
if (q.size() != 0)
available.signalAll(); // 2.3.2
return x;
}
}
}
} finally {
lock.unlock(); // 3
}
}
图10-10是DelayQueue.take()的执行示意图。
如图所示,获取任务分为3大步骤。
1)获取Lock。
2)获取周期任务。
·如果PriorityQueue为空,当前线程到Condition中等待;否则执行下面的2.2。
·如果PriorityQueue的头元素的time时间比当前时间大,到Condition中等待到time时间;否
则执行下面的2.3。
·获取PriorityQueue的头元素(2.3.1);如果PriorityQueue不为空,则唤醒在Condition中等待的所有线程(2.3.2)。
3)释放Lock。
ScheduledThreadPoolExecutor在一个循环中执行步骤2,直到线程从PriorityQueue获取到一
个元素之后(执行2.3.1之后),才会退出无限循环(结束步骤2)。
最后,让我们看看ScheduledThreadPoolExecutor中的线程执行任务的步骤4,把
ScheduledFutureTask放入DelayQueue中的过程。下面是DelayQueue.add()的源代码实现。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 1
try {
E first = q.peek();
q.offer(e); // 2.1
if (first == null || e.compareTo(first) < 0)
available.signalAll(); // 2.2
return true;
} finally {
lock.unlock(); // 3
}
}
图10-11是DelayQueue.add()的执行示意图。
如图所示,添加任务分为3大步骤。
1)获取Lock。
2)添加任务。
·向PriorityQueue添加任务。
·如果在上面2.1中添加的任务是PriorityQueue的头元素,唤醒在Condition中等待的所有线
程。
3)释放Lock。