1,概述
本文中涉及一些与线程池相关的概念,可以先看这个
在Java
中,使用线程来异步执行任务。如果为每一个任务都分配一个新线程来执行,这些线程的创建与销毁都将会消耗大量的计算资源。这种策略可能会使处于高负荷状态的应用最终崩溃
Java
的线程既是工作单元,也是执行机制。从JDK5开始,把工作单元与工作机制分离开来:工作单元分为Runnable
和Callable
,执行机制则由Executor
框架提供。
在HotSpot
虚拟机的线程模型中,Java
线程会被一对一映射为本地操作系统线程。Java
线程启动时会创建一个本地操作线程;Java
线程终止时,这个操作系统线程也会被回收,通常操作系统会调度所有线程并将它们分配给可用的CPU
。
而在上层,Java
多线程程序会将应用分解为若干个任务,然后通过用户级的调度器(Executor
框架)将这些任务映射为固定数量的线程;而在底层,操作系统将这些线程映射到硬件处理器上。相关的示意图如下所示:
上图可以这样理解:Java
多线程程序将应用分解为若干个任务,任务被包装成Runnable
或者Callable
,交由Executor
框架;Executor
框架会将这些任务传给线程池进行执行;而线程池中的每个线程都有一个操作系统线程与之对应,操作系统则通过线程调度程序将这些操作系统线程交由CPU
执行。
2,Executor框架的结构
- 任务: 包括被执行任务需要执行的接口:
Runnable
接口或Callable
接口 - 任务的执行:包括任务执行机制的核心接口:
Executor
,以及继承自Executor
接口的ExecutorService
接口(两个关键类:ThreadPoolExecutor
和ScheduledThreadPoolExecutor
) - 异步计算的结果:任务执行完后返回的结果,包括接口
Future
和实现Future
接口的FutureTask
类
关于Exexutor
框架包含的主要的类和接口如下图所示:
Executor
框架的使用示意图:
- 主线程首先创建实现
Runnable
或者Callable
接口的任务对象。工具类Executors
可以将Runnable
对象封装成为一个Callable
对象。值得一提的是:因为FutureTask
也实现了Runnable
接口,所以也可以创建一个FutureTask
交给ExecutorService
执行 - 将
Runnable
对象直接交给ExecutorService
执行(调用ExecutorService.execute
)或者把Runnable / Callable
提交给Executor
执行(调用ExecutorService.submit(Runnable)
或者ExecutorService.submit(Callable)
) - 如果执行
ExecutorService.submit(...)
那么ExecutorService
会返回一个实现了Future
接口的对象(到目前为止返回的是FutureTask
,但在源代码中,返回值的类型是Future
,所以需要注意) - 主线程执行
FutureTask.get()
等到任务执行完成,或者可以执行FutureTask.cancel(...)
取消任务的执行
3,Executor框架成员
Executor
框架的主要成员有:
Runnable
接口Callable
接口Executors
ThreadPoolExecutor
ScheduledThreadPoolExecutor
Future
接口
3.1,Runnable&Callable
Runnable
接口与Callable
接口的实现类,都可以被ThreadPoolExecutor
或ScheduledThreadPoolExecutor
执行。它们的区别在于:Runnable
不会返回结果,而Callable
可以返回结果。
可以通过工厂类Executors
把一个Runnable
包装成一个Callable
:
public static Callable<Object> callable(Runnable task) //假设这个方法返回的是对象Callable1
或是将一个Runnable
和一个待返回的结果包装成一个Callable
:
public static <T> Callable<T> callable(Runnable task, T result)//假设这个方法返回的是对象Callable2
当我们把Callable
交给线程池执行时,执行submit(...)
会返回一个FutureTask
对象,主线程可以通过FutureTask.get()
等待任务执行完成,执行完成后FutureTask.get()
将返回结果。如果是向线程池提交对象Callable1
的话,那么将返回null
;如果向线程池提交对象Callable2
,那么将返回result
对象。
3.2,Executors
Executors
在Executor
框架中扮演的时线程工厂的角色,可以通过Executors
创建特定功能的线程池:
public class Executors {
...
//创建固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
...
//创建单个线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
...
}
3.3,ThreadPoolExecutor
ThreadPoolExecutor
通常使用工厂类Executors
来创建。Executors
可以创建3种类型的ThreadPoolExecutor
:
SingleThreadExecutor
:单个线程的线程池FixedThreadPool
:固定线程数的线程池CachedThreadPool
:根据需要创建新线程的线程池
参数说明
创建线程池时需要传入的相关参数:
corePoolSize
:核心线程池的大小maximumPoolSize
:最大线程池的大小BlockingQueue
:用来暂时保存任务的工作队列RejectedExecutionHandler
:当ThreadPoolExecutor
已经关闭或者已经饱和之后,execute()
方法将要调用的Handler
keepAliveTime
:创建出来的工作线程(非核心线程池里面的线程)空闲多久之后将会被销毁
工作原理
值得一提的是,线程池的基本工作原理:
- 当有任务进来时,判断核心线程池里面的线程数是否达到
corePoolSize
指定的数量 - 如果未达到,那么就创建核心线程,并执行任务(这一步被称为线程池的预热)
- 如果达到,那么就将其加入任务队列;每当有新的任务进来时,就将任务加入任务队列中
- 当任务队列满了之后,线程池判断此时新创建线程是否会导致线程池内的线程数量大于
maximumPoolSize
指定的数量 - 如果不会,那么创建工作线程并从任务队列中拉取任务执行
- 如果会,那么执行拒绝策略
3.3.1,FixedThreadPool
FixedThreadPool
被称为可重用固定线程数的线程池。相关的创建方法:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池的corePoolSize
和maximumPoolSize
都被设置为nThreads
,并且任务队列选择的是LinkedBlockingQueue
,队列的容量为Integer.MAX_VALUE
,也就是无界队列。
这就导致了,当FixedThreadPool
的线程数量达到corePoolSize
之后,新来的任务将会进入任务队列中等待(实际上,当选择的任务队列是无界队列之后,maximumPoolSize
这一参数就已经失效了;再加上corePoolSize和maximumPoolSize
数值相同,keepAliveTime
也将是一个无效参数)
而且由于是无界队列,所以也不会触发线程池的拒绝策略
FixedThreadPool
的执行流程如图所示:
- 如果当前运行的线程数少于
corePoolSize
,则创建新线程来执行任务 - 在线程池完成预热之后(当前运行的线程数等于
corePoolSize
),将任务加入LinkedBlockingQueue
- 当线程执行完1中的任务之后,会循环地从任务队列中获取任务进行执行
3.3.2,SingleThreadExecutor
SingleThreadExecutor
是使用某个worker
线程的Executor
,相关的创建方法:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadExecutor
的corePoolSize
和maximumPoolSize
被设置为1,其他参数跟FixedThreadPool
相同。使用LinkedBlockingQueue
作为任务队列带来的影响同FixedThreadPool
相同。
相关的运行示意图如下:
- 如果当前运行的线程数少于
corePoolSize
(即线程池中无运行的线程)那么创建一个新线程来执行 - 在线程完成预热之后(当前线程池中有一个运行的线程),将任务加入
LinkedBlockingQueue
- 线程执行完1中的任务之后,会循环的从
LinkedBlockingQueue
获取任务来执行
3.3.3,CachedThreadPool
CachedThreadPool
是一个会根据需要创建新线程的线程池,相关的创建代码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
corePoolSize
被设置为0,maximumPoolSize
被设置为Integer.MAX_VALUE
,意味着线程池允许创建无限多的线程
任务队列使用 SynchronousQueue
,这是一种无容量的阻塞队列,其作用为:每个插入操作必须等待另一个线程的对应移除操作(每当有任务进来时,不会贮存在队列中,而是必须有一个线程来领取这个任务)。
keepAliveTime
被设置为60,意味着CachedThreadPool
内的空闲线程等待新任务的最长时间为60秒,60秒后将会被终止
值得注意的是:CachedThreadPool
的SynchronousQueue
是无容量的阻塞队列,并且maximumPoolSize
为Integer.MAX_VALUE
,意味着当向线程池提交任务的速度快于线程池执行任务的速度,CachedThreadPool
会不断的创建新线程。极端情况下会导致因为创建过多的线程而耗尽CPU
和内存资源
相关的执行示意图如下:
- 首先执行
SynchronousQueue.offer(Runnable task)
。如果当前有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)
,那么主线程执行offer
操作与空闲线程执行poll
操作配对成功,主线程会把任务交给空闲线程执行,execute()
执行完成;否则执行步骤2 - 当初始
maximumPool
为空,或者没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)
,这时候CachedThreadPool
会创建一个新的线程来执行任务,execute()
执行完成 - 当步骤2中创建的线程执行完任务之后,会再次执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)
,这个方法会让线程等待60秒钟,如果60秒钟内主线程提交了一个新任务过来,将由这个空闲线程执行新任务;否则空闲线程将终止。由于空闲60秒的线程将会被终止,所以长期保持空闲的CachedThreadPool
不会使用任何资源
CachedThreadPool
中的任务传递示意图如下:
3.4,ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
,用于在给定的延迟时间后运行任务,或者是定期执行任务。ScheduledThreadPoolExecutor
的功能与Timer
类似,但比其功能更强大、更灵活;Timer
对应的是单个后台线程,而ScheduledThreadPoolExecutor
则可以指定多个对应的后台线程数
3.4.1,ScheduledThreadPoolExecutor运行机制
ScheduledThreadPoolExecutor
的执行示意图如下所示:
- 当调用
ScheduledThreadPoolExecutor
的scheduleAtFixedRate()
或者scheduleWithFixedDelay()
,会向ScheduledThreadPoolExecutor
的DelayQueue
添加一个实现了RunnableScheduledFuture
接口的ScheduledFutureTask
- 线程池的线程从
DelayQueue
中获取ScheduledFutureTask
,然后执行任务
为了实现周期性的执行任务,ScheduledThreadPoolExecutor
对ThreadPoolExecutor
做出了如下的修改:
- 使用
DelayQueue
作为任务队列 - 获取任务的方式不同
- 执行周期任务后,增加了额外的处理
3.4.2,ScheduledThreadPoolExecutor的实现
ScheduledThreadPoolExecutor
会将待调度的任务(ScheduledFutureTask
)放到一个DelayQueue
中
每个ScheduledFutureTask
包含三个成员变量:
time
:这个任务将要被执行的具体时间sequenceNumber
:这个任务被加入到线程池中的序号period
:任务执行的间隔周期
DelayQueue
封装了一个PriorityQueue
(优先级队列),这个PriorityQueue
会对队列中的ScheduledFutureTask
进行排序。排序时按照time
从小到大的顺序,时间早的会被放到队头方向,时间相同的话;会根据sequenceNumber
,将序号小的排到前面。
ScheduledThreadPoolExecutor
的执行示意图:
- 线程1从
DelayQueue
中获取已经到期的ScheduledThreadFutureTask
(通过执行DelayQueue.take()
进行获取)。到期任务指的是ScheduledFutureTask
的time
小于等于当前时间 - 线程1执行这个
ScheduledFutureTask
- 线程1修改
ScheduledFutureTask的time
变量为下次将要被执行的时间 - 线程1把这个修改
time
之后的ScheduledFutureTask
放回DelayQueue
中(通过DelayQueue.add()
)
3.5,FutureTask
Future
接口和实现Future
接口的FutureTask
类,代表异步计算的结果
FutureTask
除了实现Future接口外,还实现了Runnable
接口。因此可以将一个FutureTask
交给Executor
执行,也可以直接FutureTask.run()
直接执行。根据FutureTask.run()
的执行实际,FutureTask
可以处于下面3中状态:
- 未启动:
FutureTask.run()
未被执行,FutureTask
处于未启动状态 - 已启动:
FutureTask.run()
方法被执行过程中,FutureTask
就处于已启动状态 - 已完成:
FutureTask.run()
方法执行完后正常结束,或者被取消(执行FutureTask.cancel()
)又或者FutureTask.run()
抛出异常或者异常结束,FutureTask
处于已完成状态
在不同状态执行不同的方法会造成不同的结果:
FutureTask
处于未启动或者已启动时,执行FutureTask.get()
将导致线程阻塞- 当
FutureTask
处于已完成状态,执行FutureTask.get()
将会导致线程立即返回结果或者抛出异常 - 当
FutureTask
处于未启动状态,执行FutureTask.cancel()
将导致任务不会被执行 - 当
FutureTask
处于已启动状态,执行FutureTask.cancel(true)
将通过中断线程来试图停止任务;如果执行FutureTask.cancel(false)
将不会对执行此任务的线程造成影响 - 当
FutureTask
处于已完成状态时,执行FutureTask.cancel(...)
将返回false