1. Executor框架简介
① Java任务调度的两级调度模型
- 在HotSpot VM的线程模型中,Java线程(
java.lang.Thread
)被一对一的映射为本地操作系统线程:Java线程启动时会创建一个本地操作系统线程;该Java线程终止时,本地操作系统线程也会被回收。操作系统会调度所有线程并把他们分配给可用的CPU。 - Java任务调度的两级调度模型:
- 在上层,应用程序的被分解为若干个任务,这些任务被用户级的调度器(Executor框架)映射为固定数量的线程。
- 在底层,操作系统内核将这些线程映射到CPU上。
- 应用程序可以通过Executor框架控制上层的调度,而底层的调度由操作系统内核完成,不受应用程序的控制。
② Executor框架的结构
Executor框架主要由三部分组成:
- 任务: 被执行任务需要实现Runnable接口或者Callable接口。
- Runnable接口和Callable接口都可以被ThreadPoolExecutor和ScheduledThreadPoolExecutor执行。
- 两者的区别:
① Runnable接口不会返回结果,而Callable接口可以返回结果;
② Runnable对象可以通过Executors工具类中的Executors.callable(Runnable task)
或者Executors.callable(Runnable task, Object result)
转化为Callable对象。
- 任务的执行: 包括任务执行机制的核心接口 Executor ,以及继承自Executor 接口的
ExecutorService接口
。Executor框架有两个关键类ThreadPoolExecutor类
和ScheduledThreadPoolExecutor类
,实现了ExecutorService接口。
- 其中,
ScheduledThreadPoolExecutor类
继承了ThreadPoolExecutor类
,并实现了ScheduledExecutorService接口
。 - 而
ScheduledExecutorService接口
又实现了ExecutorService接口
。
- 异步计算的结果:
Future接口
和实现了Future接口的FutureTask类
。
- 将实现Runnable接口或者Callable接口的任务对象(通过
ExecutorService.submit()方法
)提交给ThreadPoolExecutor
或者ScheduledThreadPoolExecutor
去执行,会返回一个FutureTask对象。
Executor框架主要成员的介绍:
Executor接口:
Executor框架的基础,将任务的提交和任务的执行分离。ThreadPoolExecutor类:
Java线程池的核心实现类,用来执行提交的任务。ScheduledThreadPoolExecutor类:
Executor框架的另一个关键类,可以在给定的延迟后执行命令,或者定期执行命令。- 还有
Future接口
、FutureTask类
、Runnable接口
、Callable接口
。
③ Executor框架的使用示意图
- 主线程首先创建实现Runnable或者Callable接口的任务对象。
- 工具类Executors可以实现将Runnable对象封装成一个Callable对象,方法:
Executors.callable(Runnable task)
或Executors.callable(Runnable task,Object result)
。
- 然后可以把创建完成的Runnable对象直接交给ThreadPoolExecutor或者ScheduledThreadPoolExecutor执行,方法:
ThreadPoolExecutor.execute(Runnable command)
或者ScheduledThreadPoolExecutor.execute(Runnal command)
;或者也可以把Runnable对象或Callable对象提交给ExecutorService执行,方法:ExecutorService.submit(Runnable task)
或ExecutorService.submit(Callable task)
。
- 执行execute()方法和submit()方法的区别是什么呢?
①execute()方法
用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功;
②submit()方法
用于提交需要返回值的任务,线程池会返回一个FutureTask类型的对象,通过这个FutureTask对象可以判断任务是否执行成功。
③ 对于需要要返回值的任务,可以通过FutureTask.get()方法
或者FutureTask.get(long timeout, TimeUnit unit)方法
来获取返回值。其中,前者会阻塞当前线程直到任务完成;后者,会阻塞当前线程一段时间后立即返回,有可能任务并没有执行完成。
- 最后,主线程可以执行
FutureTask.get()方法
来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)
来取消任务的执行。
2. Java常见的线程池
① 工厂类Executors创建线程池
FixedThreadPool
- 使用固定线程数的线程池,适用于为了满足资源管理的需求,而限制当前线程数的应用场景。或者是适用于负载比较重的服务器。
- 创建方式:
Executors.newFixedThreadPool(threadsNum)
。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
SingleThreadExecutor
- 使用单个线程的线程池,适用于需要保证各个任务顺序执行,而且在任意时刻,不会有多个活动线程的应用场景。
- 创建方式:
Executors.newSingleThreadExecutor()
。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
CachedThreadPool
- 根据需要创建新线程的线程池,适用于执行很多短期异步任务的小程序,或者是负载比较轻的服务器。
- 创建方式:
Executors.newCachedThreadPool()
。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
ScheduledThreadPool
- 使用固定线程数的线程池,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求,需要限制后台线程数量的应用场景。
- 创建方式:
Executors.newScheduledThreadPool(corePoolSize)
。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
SingleThreadScheduledExecutor
- 使用单个线程的线程池,适用于需要使用单个后台线程执行周期任务,同时需要保证各个任务执行顺序的应用场景。
- 创建方式:
Executors.newSingleThreadScheduledExecutor()
。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
② FixedThreadPool
- 使用固定线程数的线程池,具有以下特点:
corePoolSize = maximumPoolSize = newFixedThreadPool(num)方法的参数num
,实际只使用了corePool。keepAliveTime
的值为0L,表明工作线程如果空闲会被立即终止。- 使用
LinkedBlockingQueue
,这是一种基于链表的无界队列,因为队列容量为Integer.MAX_VALUE
。队列中的元素采用FIFO序。 - 使用无界队列,FixedThreadPool在未调用
shutdown()或者shutdownNow()方法
的情况下,不会调用饱和策略。
- 关于无界队列:
- 当
corePoolSize
满时,由于使用无界队列,新提交的任务总是可以存放在阻塞队列中。因此,线程池中的线程数永远不会超过corePoolSize
。 - 由于1,使用无界队列使得线程池中的
maximumPoolSize
参数无效。 - 由于1和2,使用无界队列使得线程池中的
keepAliveTime
参数无效。 - 由于使用无界队列,FixedThreadPool在未调用
shutdown()或者shutdownNow()方法
的情况下,不会调用饱和策略(即不会RejectExecutionHandler.rejectExecution()
方法)。
- FixedThreadPool的工作流程:
FixedThreadPool 的 execute() 运行示意图
- 如果当前运行的线程数小于
corePoolSize
,则创建新的线程来执行任务; - 当前运行的线程数等于
corePoolSize
,将任务加入LinkedBlockingQueue
; - 工作线程执行完当前的任务后,不会销毁,而是循环地从
LinkedBlockingQueue
中获取任务来执行;
③ SingleThreadExecutor
- 使用单个线程的线程池,具有以下特点:
corePoolSize = maximumPoolSize = 1
,实际只使用了corePool。keepAliveTime
的值为0L,表明工作线程如果空闲会被立即终止。- 使用
LinkedBlockingQueue
,基于链表的无界队列,队列中的元素采用FIFO顺序。 - 使用无界队列,SingleThreadExecutor在未调用
shutdown()或者shutdownNow()方法
的情况下,不会调用饱和策略。
- SingleThreadExecutor的工作流程:
SingleThreadExecutor 的 execute() 运行示意图
- 如果线程池中无运行的线程,则创建一个新的线程执行任务。
- 如果线程池中已经有正在运行的线程,则将任务加入到
LinkedBlockingQueue
中。 - 工作线程执行完当前的任务后,不是立即销毁,而是循环地从
LinkedBlockingQueue
中取出任务来执行。
- 与FixedThreadPool的区别:
SingleThreadExecutor
线程池中同时最多只有一个线程活跃,同时最多只有一个任务在执行。
④ CachedThreadPool
- 可以根据需要创建新线程的线程池,具有以下特点:
corePoolSize = 0
,maximumPoolSize = Integer.MAX_VALUE
,表明线程池无界,实际只使用了maximumPool。keepAliveTime
的值为60s,表明工作线程最多允许空闲60s,超过60s会被终止。- 使用
SynchronousQueue
,是一种没有容量的阻塞队列,主线程的每个添加操作必须等待另一个线程的移除操作,反之亦然。 - CachedThreadPool在线程池中线程数等于maximumPoolSize时,会调用饱和策略。
- CachedThreadPool的工作流程:
CachedThreadPool 的 execute() 运行示意图
- 主线程首先执行
Synchronous.offer()
,即添加操作。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)
(即移除操作),那么主线程的offer操作和空闲线程的poll操作配对成功,主线程把任务交给空闲线程执行。 - 如果maximumPool为空,或者maximumPool没有空闲线程,则offer操作和poll操作匹配失败。此时,CachedThreadPool会创建新的线程执行该任务。
- 工作线程执行完当前线程后,会执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)
,这个poll操作会让空闲线程等待60秒。如果60秒内主线程发起了offer操作,这个空闲线程便会执行主线程新提交的任务;否则,这个空闲线程将会终止。
⑤ ScheduledThreadPoolExecutor
- 有固定数量的线程池,可以在给定的延迟后执行任务,或者定期执行任务。具有以下特点:
corePoolSize = Executors.newScheduledThreadPool(num)中的参数
,ScheduledThreadPool的参数大于等于1,而SingleThreadScheduledExecutor的参数为1。maximumPoolSize = Integer.MAX_VALUE
,实际只使用了corePool。keepAliveTime
的值为0L,表明线程一旦空闲就会被终止。- 使用
DelayQueue
,这是一个具有优先级的无界队列,内部封装了一个PriorityQueue
,可以实现任务的定期执行和延后执行。 - 使用无界队列,ScheduledThreadPool在未调用
shutdown()或者shutdownNow()方法
的情况下,不会调用饱和策略。
- ScheduledThreadPool的运行流程:
ScheduledThreadPoolExecutor 的 execute() 运行示意图
- 当调用
ScheduledThreadPoolExecutor
的scheduleAtFixedRate() 方法
或者scheduleWithFixedDelay() 方法
时,会向DelayQueue
添加一个实现了RunnableScheduledFuture 接口
的ScheduledFutureTask
。 - 线程池中的线程从
DelayQueue
中获取ScheduledFutureTask
,然后执行任务。
- 与ThreadPoolExecutor的区别:
- 使用DelayQueue作为阻塞队列
- 获取任务的方式不同
- 执行周期任务后,添加了额外的处理
执行任务周期的步骤,或者如何实现任务周期执行的?
- 预备知识:ScheduledFutureTask中的三个重要的成员变量:
- long time: 表示这个任务将要被执行的具体时间。当time
大于等于
当前系统的时间,表明该任务已到期,可以执行。 - long sequenceNumber: 表示这个任务被添加到
ScheduledThreadPoolExecutor
的序号。 - long period: 表示任务执行的时间间隔
- DelayQueue中封装的PriorityQueue,会对这些任务按time进行排序,时间小的先被执行。如果time相同,则按seq进行排序,序号较小的先执行。(即如果任务的time相同,则先提交的先被执行)
ScheduledThreadPoolExecutor执行任务周期的步骤
- 线程1使用
DelayQueue.take()方法
从DelayQueue中获取已经到期的ScheduledFutureTask
。到期任务是指ScheduledFutureTask
的time小于等于当前系统的时间。
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
- 线程1执行这个
ScheduledFutureTask
。 - 线程1修改
ScheduledFutureTask
中的time为下次将要被执行的时间。 - 线程1将修改了time后的
ScheduledFutureTask
使用DelayQueue.add()方法
,放回到DelayQueue中。
3. 关于常见线程池的问题
1. 说一下常见的四种线程池及区别
- 基础回答: 常见的四种线程池有哪些、如何通过Executors工具类创建
- 进阶: 每个线程池的参数设置(线程池大小、空闲线程的存活时间、使用的阻塞队列、是否调用饱和策略);每个线程池的运行流程
- 补充讲解: 无界队列对线程池的影响
2. 线程池的底层实现?
- 讲解Executor框架:三个组成部分、接口和类之间的关系、线程池的总体运行流程、四种常见的线程池