并发编程-线程池原理与实战
文章目录
- 并发编程-线程池原理与实战
-
- 一、为什么要使用线程池
-
- [1.1 为什么需要使用线程池](#1.1 为什么需要使用线程池)
- [1.2 使用线程池带来的好处](#1.2 使用线程池带来的好处)
- [1.3 线程池的工作流程](#1.3 线程池的工作流程)
- [1.4 线程池的状态转换](#1.4 线程池的状态转换)
- 二、JUC的线程池架构
-
- [2.1 Executor](#2.1 Executor)
- [2.2 ExecutorService](#2.2 ExecutorService)
- [2.3 AbstractExecutorService](#2.3 AbstractExecutorService)
- [2.4 ThreadPoolExecutor](#2.4 ThreadPoolExecutor)
- [2.5 ScheduledExecutorService](#2.5 ScheduledExecutorService)
- [2.6 ScheduledThreadPoolExecutor](#2.6 ScheduledThreadPoolExecutor)
- [2.7 Executors](#2.7 Executors)
- [三、Executors 的 4 种快捷创建线程池的方法](#三、Executors 的 4 种快捷创建线程池的方法)
-
- [3.1 newSingleThreadExecutor 创建"单线程化线程池"](#3.1 newSingleThreadExecutor 创建“单线程化线程池”)
- [3.2 newFixedThreadPool 创建"固定数量的线程池"](#3.2 newFixedThreadPool 创建“固定数量的线程池”)
- [3.3 newCachedThreadPool 创建"可缓存线程池"](#3.3 newCachedThreadPool 创建“可缓存线程池”)
- [3.4 newScheduledThreadPool 创建"可调度线程池"](#3.4 newScheduledThreadPool 创建“可调度线程池”)
- [3.5 为什么线程池不允许使用Executors去创建? 推荐方式是什么?](#3.5 为什么线程池不允许使用Executors去创建? 推荐方式是什么?)
- 四、线程池的标准创建方式及参数说明
-
- [4.1 标准创建线程池方法](#4.1 标准创建线程池方法)
- [4.2 核心线程数和最大线程数](#4.2 核心线程数和最大线程数)
- [4.3 空闲线程存活时间](#4.3 空闲线程存活时间)
- [4.4 任务阻塞队列](#4.4 任务阻塞队列)
- [4.5 线程工厂 ThreadFactory](#4.5 线程工厂 ThreadFactory)
- [4.6 线程池的拒绝策略](#4.6 线程池的拒绝策略)
- [4.7 创建一个标准线程池示例](#4.7 创建一个标准线程池示例)
- 五、向线程池提交任务的两种方式
-
- [5.1 两种提交任务到线程池的方式](#5.1 两种提交任务到线程池的方式)
- [5.2 通过 submit()返回的 Future 对象获取结果示例](#5.2 通过 submit()返回的 Future 对象获取结果示例)
- [5.3 通过 submit()返回的 Future 对象捕获异常示例](#5.3 通过 submit()返回的 Future 对象捕获异常示例)
- 六、线程池的任务调度流程
-
- [6.1 总体的线程池的任务调度流程](#6.1 总体的线程池的任务调度流程)
- [6.2 线程池任务的执行](#6.2 线程池任务的执行)
- [6.3 execute()方法流程分析](#6.3 execute()方法流程分析)
- [6.4 源码解析------addWorker(Runnable firstTask,boolean core)](#6.4 源码解析——addWorker(Runnable firstTask,boolean core))
- [6.5 源码解析------ runWorker(Worker w)](#6.5 源码解析—— runWorker(Worker w))
- [6.6 源码解析------ getTask()](#6.6 源码解析—— getTask())
- 七、调度器的钩子方法
- 八、线程池的状态
- 九、关闭线程池
-
- [9.1 shutdown()方法原理](#9.1 shutdown()方法原理)
- [9.2 shutdownNow()方法的原理](#9.2 shutdownNow()方法的原理)
- [9.3 awaitTermination()方法的使用](#9.3 awaitTermination()方法的使用)
- [9.4 优雅地关闭线程池](#9.4 优雅地关闭线程池)
- [9.5 注册 JVM 钩子函数自动关闭线程池](#9.5 注册 JVM 钩子函数自动关闭线程池)
- 十、确定线程池的线程数
-
- [10.1 按照任务类型对线程池进行分类](#10.1 按照任务类型对线程池进行分类)
- [10.2 为 IO 密集型任务确定线程数](#10.2 为 IO 密集型任务确定线程数)
- [10.3 为 CPU 密集型任务确定线程数](#10.3 为 CPU 密集型任务确定线程数)
- [10.4 为混合型任务确定线程数](#10.4 为混合型任务确定线程数)
- 十一、源码解析-ThreadPoolExecutor
-
- [11.1 关键属性](#11.1 关键属性)
- [11.2 内部状态](#11.2 内部状态)
- [11.3 任务执行](#11.3 任务执行)
- [11.4 任务提交](#11.4 任务提交)
- [11.5 任务关闭](#11.5 任务关闭)
- 十二、源码解析-ScheduledThreadPoolExecutor
-
- [12.1 ScheduledThreadPoolExecutor类结构图](#12.1 ScheduledThreadPoolExecutor类结构图)
- [12.2 ScheduledThreadPoolExecutor 核心属性](#12.2 ScheduledThreadPoolExecutor 核心属性)
- [12.3 ScheduledThreadPoolExecutor构造方法](#12.3 ScheduledThreadPoolExecutor构造方法)
- [12.4 ScheduledThreadPoolExecutor核心方法](#12.4 ScheduledThreadPoolExecutor核心方法)
-
- 1、schedule()方法
- [2、 scheduleAtFixedRate 和 scheduleWithFixedDelay](#2、 scheduleAtFixedRate 和 scheduleWithFixedDelay)
- 3、shutdown()
- [12.5 ScheduledThreadPoolExecutor内部类ScheduledFutureTask](#12.5 ScheduledThreadPoolExecutor内部类ScheduledFutureTask)
一、为什么要使用线程池
1.1 为什么需要使用线程池
Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作:
- 必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。
- 需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。
Java高并发应用频繁创建和销毁线程的操作将是非常低效的,而且是不被编程规范所允许的。
1.2 使用线程池带来的好处
1.3 线程池的工作流程
线程池的工作原理大致分为4步,如下图所示:
- 如果线程池中的线程数小于核心线程数,则创建新线程来执行提交的任务。
- 如果线程池中的线程数大于等于核心线程数,则提交的任务被放入任务阻塞队列中。
- 当任务队列放满了之后,再提交新的任务则创建新线程执行提交的任务。
- 当线程池中的线程数大于等于最大线程数时,再提交任务,线程池则执行拒绝策略方法。
1.4 线程池的状态转换
二、JUC的线程池架构
2.1 Executor
Executor是Java异步目标任务的"执行者"接口,其目标是来执行目标任务。包含一个函数式方法execute(),用来执行提交的Runnable类型的任务。
Java
public interface Executor {
void execute(Runnable command);
}
2.2 ExecutorService
ExecutorService继承于Executor。它是Java异步目标任务的"执行者服务"接口,对外提供异步任务的接收服务,ExecutorService提供了"接收异步任务并转交给执行者"的方法,如submit系列方法、invoke系列方法等。
Java
public interface ExecutorService extends Executor {
//关闭线程池
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
//向线程池提交单个异步任务
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
//向线程池批量提交异步任务
<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;
<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;
}
2.3 AbstractExecutorService
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。
Java
public abstract class AbstractExecutorService implements ExecutorService {
//......
}
2.4 ThreadPoolExecutor
ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。
java
public class ThreadPoolExecutor extends AbstractExecutorService {
// .....
}
2.5 ScheduledExecutorService
ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成"延时"和"周期性"任务的调度线程池接口,其功能和Timer/TimerTask类似。
java
public interface ScheduledExecutorService extends ExecutorService {
//......
}
2.6 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中"延时执行"和"周期执行"等抽象调度方法的具体实现。ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。
java
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
implements ScheduledExecutorService {
//......
}
2.7 Executors
Executors 是 个 静 态 工 厂 类 , 它 通 过 静 态 工 厂 方 法 返 回 ExecutorService 、ScheduledExecutorService等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。
java
public class Executors {
// 创建固定数量线程的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
}
三、Executors 的 4 种快捷创建线程池的方法
Java通过Executors工厂类提供4种快捷创建线程池的方法:
四种方式存在的潜在问题:
3.1 newSingleThreadExecutor 创建"单线程化线程池"
该方法用于创建一个"单线程化线程池",也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
Java
//创建只有一个线程的线程池方法一
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
//本质还是调用标准化创建线程池的方法构造ThreadPoolExecutor线程池对象
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//创建只有一个线程的线程池方法二:需要指定生成该线程的线程工厂ThreadFactory
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
使用示例代码:
Java
//测试用例:只有一条线程的线程池
@Test
public void testSingleThreadExecutor() {
//创建只有一个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池:执行shutdown()方法后,线程池状态变为SHUTDOWN状态,此时线程池将拒绝新任务,不能再往线程池中添加新任务,否则会抛出RejectedExecutionException异常。此时,线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成才会退出。还有一个与shutdown()类似的方法,叫作shutdownNow(),执行shutdownNow()方法后,线程池状态会立刻变成STOP,并试图停止所有正在执行的线程,不再处理还在阻塞队列中等待的任务,会返回那些未执行的任务。
pool.shutdown();
}
//TargetTask.java:异步的执行目标类
public class TargetTask implements Runnable {
public static final int SLEEP_GAP = 500;
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
public void run() {
Print.tco("任务:" + taskName + " doing");
// 线程睡眠一会
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
Print.tco(taskName + " 运行被异常打断." + e.getMessage());
}
Print.tco(taskName + " 运行结束.");
}
@Override
public String toString() {
return "TargetTask{" + taskName + '}';
}
}
执行结果及分析
shell
11:19:07.349 [pool-1-thread-1] INFO com.crazymakercircle.threadpool.SeqOrScheduledTargetThreadPoolLazyHolder - 线程池已经初始化
[pool-1-thread-1]:任务:task-1 doing
[pool-1-thread-1]:task-1 运行结束.
[pool-1-thread-1]:任务:task-2 doing
[pool-1-thread-1]:task-2 运行结束.
.......
[pool-1-thread-1]:任务:task-9 doing
[pool-1-thread-1]:task-9 运行结束.
[pool-1-thread-1]:任务:task-10 doing
[pool-1-thread-1]:task-10 运行结束.
单个线程的线程池具有以下特点:
- 单线程线程池中的任务是按照任务的提交顺序执行的。
- 线程池中的唯一线程是无限存活的。
- 当线程池中的唯一线程在执行任务时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的LinkedBlockQueue。
单线程线程池适用场景:
- 任务按照提交次序,一个任务一个任务的执行。
潜在问题
由于使用的阻塞队列是LinkedBlockingQueue的无界队列,如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽。
3.2 newFixedThreadPool 创建"固定数量的线程池"
以下方法用于创建拥有固定数量线程的线程池:
Java
//创建固定数量线程的线程池方法一:需要指定线程池的线程数量 nThreads
public static ExecutorService newFixedThreadPool(int nThreads) {
//
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//创建固定数量线程的线程池方法二:需要指定线程池的线程数量 nThreads,以及生成线程的线程工厂ThreadFactory
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用示例代码:
Java
//测试用例:只有3条线程固定大小的线程池
@Test
public void testNewFixedThreadPool() {
//快捷创建一个只有3个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
// 循环5次,向线程池提交10个异步任务
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
执行结果分析:
shell
[pool-1-thread-3]:任务:task-3 doing
[pool-1-thread-2]:任务:task-2 doing
[pool-1-thread-1]:任务:task-1 doing
[pool-1-thread-2]:task-2 运行结束.
[pool-1-thread-1]:task-1 运行结束.
[pool-1-thread-3]:task-3 运行结束.
[pool-1-thread-1]:任务:task-4 doing
[pool-1-thread-2]:任务:task-5 doing
[pool-1-thread-3]:任务:task-6 doing
[pool-1-thread-3]:task-6 运行结束.
[pool-1-thread-1]:task-4 运行结束.
[pool-1-thread-2]:task-5 运行结束.
[pool-1-thread-3]:任务:task-7 doing
[pool-1-thread-2]:任务:task-8 doing
[pool-1-thread-1]:任务:task-9 doing
[pool-1-thread-3]:task-7 运行结束.
[pool-1-thread-2]:task-8 运行结束.
[pool-1-thread-1]:task-9 运行结束.
[pool-1-thread-3]:任务:task-10 doing
[pool-1-thread-3]:task-10 运行结束.
从输出结果可以看到,该线程池同时只能执行3个任务,剩余的任务会排队等待。
固定数量线程的线程池具有以下特点:
- 如果线程数没有达到"固定数量",每次提交一个任务池内就创建一个新线程,直到线程达到线程池固定的数量。
- 线程池的大小一旦达到"固定数量"就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
固定数量线程的线程池适用场景:
需要任务长期执行的场景。"固定数量的线程池"的线程数能够比较稳定保证一个数,避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间使用的情况下,能确保尽可能少地分配线程。
潜在问题:
其潜在问题仍然存在与其workQueue属性上,该属性的值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致JVM的OOM异常,甚至造成内存资源耗尽。
3.3 newCachedThreadPool 创建"可缓存线程池"
以下的方法可用于创建可缓存线程池,如果线程池内的某些线程无事可干成为空闲线程,"可缓存线程池"可灵活回收这些空闲线程。
Java
//创建可缓存线程池方法一
public static ExecutorService newCachedThreadPool() {
//实质是通过标准创建线程池方法创建,指定的核心线程数量为0
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>());
}
//创建可缓存线程池方法二:需要指定生成线程的工厂类ThreadFacotry
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
使用示例:
Java
//测试用例:"可缓存线程池"
@Test
public void testNewCacheThreadPool() {
//创建一个可缓存线程池对象
ExecutorService pool = Executors.newCachedThreadPool();
//向线程池提交10个异步执行任务
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
执行结果:
shell
[pool-1-thread-6]:任务:task-6 doing
[pool-1-thread-9]:任务:task-9 doing
[pool-1-thread-4]:任务:task-4 doing
[pool-1-thread-1]:任务:task-1 doing
[pool-1-thread-3]:任务:task-3 doing
[pool-1-thread-2]:任务:task-2 doing
[pool-1-thread-8]:任务:task-8 doing
[pool-1-thread-5]:任务:task-5 doing
[pool-1-thread-10]:任务:task-10 doing
[pool-1-thread-7]:任务:task-7 doing
[pool-1-thread-10]:task-10 运行结束.
[pool-1-thread-5]:task-5 运行结束.
[pool-1-thread-7]:task-7 运行结束.
[pool-1-thread-2]:task-2 运行结束.
[pool-1-thread-1]:task-1 运行结束.
[pool-1-thread-4]:task-4 运行结束.
[pool-1-thread-6]:task-6 运行结束.
[pool-1-thread-9]:task-9 运行结束.
[pool-1-thread-8]:task-8 运行结束.
[pool-1-thread-3]:task-3 运行结束.
可缓存线程池特点:
- 在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
- 此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
可缓存线程池适用场景:
需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。"可缓存线程池"的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
潜在问题:
使用Executors创建的"可缓存线程池"的潜在问题存在于其最大线程数量不设上限。由于其maximumPoolSize的值为Integer.MAX_VALUE(非常大),可以认为是无限创建线程的,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。
3.4 newScheduledThreadPool 创建"可调度线程池"
通过下列方法,可以创建一个"可调度线程池",即一个提供"延时"和"周期性"任务的调度功能的ScheduledExecutorService类型的线程池。newScheduledThreadPool工厂方法可以创建一个执行"延时"和"周期性"任务可调度线程池,所创建的线程池为ScheduleExecutorService类型的实例。
Java
//以下两个方法为创建只有一个线程的可调度线程池,区别是第二个需要指定生成线程的线程工厂,适用于调度串行化任务,也就是一个任务接一个任务地串行化调度执行
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1, threadFactory));
}
//以下两个方法为创建含有指定数量线程的可调度线程池,corePoolSize为指定的线程数,第二个方法需要指定线程工厂
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
ScheduleExecutorService接口中有多个重要的接收被调目标任务方法,其中scheduleAtFixedRate和scheduleWithFixedDelay使用得比较多。
Java
//ScheduledExecutorService.java
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command, //异步任务target执行目标实例
long initialDelay, //首次执行延时
long period, //两次开始执行最小间隔时间
TimeUnit unit); //所设置的时间的计时单位,如TimeUnit.SECONDS常量
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command, //异步任务target执行目标实例
long initialDelay, //首次执行延时
long delay, //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
TimeUnit unit); //所设置的时间的计时单位,如TimeUnit.SECONDS常量
示例代码:
Java
//测试用例:"可调度线程池"
@Test
public void testNewScheduledThreadPool() {
//创建一个有两个线程的可调度线程池
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(),
0, 500, TimeUnit.MILLISECONDS);
//以上的参数中:
// 0表示首次执行任务的延迟时间,500表示每次执行任务的间隔时间
//TimeUnit.MILLISECONDS所设置的时间的计时单位为毫秒
}
sleepSeconds(1000);
//关闭线程池
scheduled.shutdown();
}
**可调度线程池适用场景:**适用于周期性执行任务的场景。
潜在问题:
使用Executors创建的"可调度线程池"的潜在问题存在于其最大线程数量不设上限。由于其线程数量不设限制,如果到期任务太多,就会导致CPU的线程资源耗尽。实际上,通过源码分析可以看出,"可调度线程池"的潜在问题首先还是无界工作队列(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM甚至耗尽内存资源的问题。
3.5 为什么线程池不允许使用Executors去创建? 推荐方式是什么?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
推荐方式1:引入commons-lang3包
Java
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
推荐方式2:引入com.google.guava包
Java
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));
//gracefully shutdown
pool.shutdown();
推荐方式3:spring配置线程池
spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可。
xml
<bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="2000" />
<property name="threadFactory" value= threadFactory />
<property name="rejectedExecutionHandler">
<ref local="rejectedExecutionHandler" />
</property>
</bean>
java
userThreadPool.execute(thread);
四、线程池的标准创建方式及参数说明
在阿里巴巴Java开发手册中强制要求创建线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor )线程池的构造方法完成的。
4.1 标准创建线程池方法
可通过ThreadPoolExecutor类的构造方法标准创建线程池,其中最重要的一个如下:
Java
public class ThreadPoolExecutor extends AbstractExecutorService {
// ....
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程池中线程的最大闲置生命周期
TimeUnit unit, // 针对keepAliveTime的时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 创建线程的线程工厂
RejectedExecutionHandler handler) { // 拒绝策略
//.....
}
//......
}
4.2 核心线程数和最大线程数
**核心线程数:**corePoolSize
**最大线程数:**maximumPoolSize
线程池执行器将会根据corePoolSize和maximumPoolSize自动地维护线程池中的工作线程,大致的规则为:
- 当在线程池接收到的新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。
- 如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。
- 当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
- corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。
4.3 空闲线程存活时间
**线程空闲时间:**keepAliveTime
**时间单位:**unit
线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长或者说保活时长,如果超过这个时间,默认情况下Idle、非Core线程会被回收。
可以通过以下方法,进行线程存活时间的动态调整:
Java
//如果池在使用过程中提交任务的频率变高,可以通过此方法将空闲时间增加,如果需要防止Idle线程被终止,可以将Idle时间设置为无限大
setKeepAliveTime(Long.MAX_VALUE,TimeUnit.NANOSECONDS);
默认情况下,超时策略仅限于非核心线程,如果想要keepAliveTime设置的idle超时策略作用于核心线程,可以通过以下方法进行设置,传入的参数为true即可:
Java
allowCoreThreadTimeOut(boolean)
4.4 任务阻塞队列
**阻塞队列:**BlockingQueue
BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。
**阻塞队列特点:**在阻塞队列为空的时候,会阻塞当前线程对阻塞队列的取数操作。即在一个线程从一个空的阻塞队列中获取元素时线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预)。
Java线程池使用BlockingQueue存放接收到的异步任务,BlockingQueue是JUC包的一个超级接口:
Java
public interface BlockingQueue<E> extends Queue<E> {}
常见的实现类如下:
4.5 线程工厂 ThreadFactory
线程工厂: ThreadFactory
ThreadFactory是Java线程工厂接口,在调用ThreadFactory的唯一方法newThread()创建新线程时,可以更改创建新线程的名称、线程组、优先级、守护进程状态等。如果newThread()返回值为null,表示线程工厂未能成功创建线程,线程池可能无法执行任何任务。
Java
public interface ThreadFactory {
//创建一个线程
Thread newThread(Runnable r);
}
在使用Executors快捷创建线程池的时候,可以传入一个ThreadFactory实例,如果不指定,则默认是使用Executors.defaultThreadFactory实例,使用默认的线程工厂实例所创建的线程全部位于同一个ThreadGroup(线程组)中,具有相同的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。
基于自定义的ThreadFactory实例创建线程池,首先需要实现一个ThreadFactory类,实现其唯一的抽象方法newThread(Runnable):
Java
//一个简单的线程工厂,实现ThreadFactory接口
static public class SimpleThreadFactory implements ThreadFactory {
static AtomicInteger threadNo = new AtomicInteger(1);
//实现其唯一的创建线程方法
@Override
public Thread newThread(Runnable target) {
String threadName = "simpleThread-" + threadNo.get();
Print.tco("创建一条线程,名称为:" + threadName);
threadNo.incrementAndGet();
//设置线程名称
Thread thread = new Thread(target, threadName);
//设置为守护线程
thread.setDaemon(true);
return thread;
}
}
4.6 线程池的拒绝策略
**拒绝策略:**RejectedExecutionHandler
在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:
- 线程池已经被关闭。
- 工作队列已满且线程数已经到达最大线程数。
无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution()方法。
RejectedExecutionHandler是拒绝策略的接口:
Java
public interface RejectedExecutionHandler {
// 拒绝执行方法
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
在JUC中RejectedExecutionHandler接口有以下几种实现:
除了提供的几种实现外,还可以自定义拒绝策略。
自定义拒绝策略:
Java
//自定义拒绝策略,实现RejectedExecutionHandler 接口
public static class CustomIgnorePolicy implements RejectedExecutionHandler {
//实现rejectedExecution 方法
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 可实现满足需要的拒绝策略
// 可做日志记录等
Print.tco(r + " rejected; " + " - getTaskCount: " + e.getTaskCount());
}
}
4.7 创建一个标准线程池示例
Java
@org.junit.Test
public void testCustomIgnorePolicy() {
int corePoolSize = 2; //核心线程数
int maximumPoolSize = 4; //最大线程数
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
//最大排队任务数
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程工厂
ThreadFactory threadFactory = new SimpleThreadFactory();
//拒绝和异常策略
RejectedExecutionHandler policy = new CustomIgnorePolicy();
//创建线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize, //核心线程数
maximumPoolSize, //最大线程数
keepAliveTime, unit, //空闲线程存活时间及时间单位
workQueue, //阻塞队列
threadFactory, //线程工厂
policy //拒绝策略
);
// 预启动所有核心线程
pool.prestartAllCoreThreads();
for (int i = 1; i <= 10; i++) {
pool.execute(new TargetTask());
}
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}
五、向线程池提交任务的两种方式
5.1 两种提交任务到线程池的方式
创建好线程池之后,就是使用线程池了,向线程池提交任务有两类方法,如下:
Java
// 方式一:调用execute方法 ThradPoolExecutor.java中
public void execute(Runnable command) {...}
// 方式二:调用submit方法,ExecutorService接口中
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
以上两类方法的区别:
-
两者接受的参数不一样
execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。
-
submit方法提交任务后会有返回值,execute方法没有
execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。
submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。
-
submit方便Exception处理
execute()方法在启动任务的执行后,任务执行过程中可能发生的异常调用者并不关心。
submit()方法返回Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。
5.2 通过 submit()返回的 Future 对象获取结果示例
submit()方法自身并不会传递结果,而是返回一个Future异步执行实例,处理过程的结果被包装到Future实例中,调用者可以通过Future.get()方法获取异步执行的结果。
Java
@Test
public void testSubmit3(){
ExecutorService threadPool = Executors.newFixedThreadPool(3);
Future<Integer> future = threadPool.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//返回200~300之间的随机数
return RandomUtil.randInRange(200, 300);
}
});
try {
//获取异步任务执行结果
Integer result = future.get();
Print.tco("异步执行的结果是:" + result);
} catch (InterruptedException e) {
Print.tco("异步调用被中断");
e.printStackTrace();
} catch (ExecutionException e) {
Print.tco("异步调用过程中,发生了异常");
e.printStackTrace();
}
sleepSeconds(10);
//关闭线程池
threadPool.shutdown();
}
5.3 通过 submit()返回的 Future 对象捕获异常示例
submit()方法自身并不会传递异常,处理过程中的异常都被包装到Future实例中,调用者在使用Future.get()方法获取执行结果时,可以捕获异步执行过程中抛出的受检异常和运行时异常,并进行对应的业务处理。
Java
//异步的执行目标类:执行过程中将发生异常
static class TargetTaskWithError extends TargetTask {
public void run() {
super.run();
throw new RuntimeException("Error from " + taskName);
}
}
//测试用例:提交和执行
@Test
public void testSubmit() {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.execute(new TargetTaskWithError());
/**
* submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。请看下面:
*/
Future future = pool.submit(new TargetTaskWithError());
try {
//如果异常抛出,会在调用Future.get()时传递给调用者
if (future.get() == null) {
//如果Future的返回为null,任务完成
Print.tco("任务完成");
}
} catch (Exception e) {
Print.tco(e.getCause().getMessage());
}
sleepSeconds(10);
//关闭线程池
pool.shutdown();
}
六、线程池的任务调度流程
6.1 总体的线程池的任务调度流程
线程池的任务调度流程(包含接收新任务和执行下一个任务)大致如下:
- 如果当前工作线程数量小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
- 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入到阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
- 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
- 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
- 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
6.2 线程池任务的执行
Java
execute()--->addWork()--->runWork(getTask)
线程池的工作线程通过Work类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。 firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源。
6.3 execute()方法流程分析
execute的整体流程如下所示:
源码如下:
Java
/*
在将来某个时候执行给定的任务。任务可以在新线程中执行,也可以在现有的池线程中执行。如果任务不能提交执行,或者因为这个执行器已经关闭,或者因为它的容量已经达到,那么任务将由当前的RejectedExecutionHandler处理。
*/
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get(); // ctl记录着runState和workCount
//1.如果线程池中的线程数workerCountOf(c)小于核心线程数,那么创建线程并执行任务command
if (workerCountOf(c) < corePoolSize) {
/**
* 在线程池中新建一个新的线程:
* 1> command:需要执行的Runnable线程
* 2> true:新增线程时,【当前活动的线程数】是否 < corePoolSize
* 3> false:新增线程时,【当前活动的线程数】是否 < maximumPoolSize
*/
if (addWorker(command, true)) return; // 添加新线程成功,则直接返回
c = ctl.get(); // 添加新线程失败,则重新获取【当前线程池中的线程数】
}
//2:如果workCount >= corePoolSize,当前线程池是RUNNING状态,则创建线程往workQueue添加线程任务,等待执行
if (isRunning(c) && workQueue.offer(command)) { // 添加command到workQueue队列中
int recheck = ctl.get(); // 重新获取ctl
// 再检查一下,当前线程池是否是RUNNING状态,如果不是,就把刚刚添加到workQueue中的command移除掉,然后调用拒绝策略
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果【当前活动的线程数为0】,则执行addWork方法
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // null表示只创建线程,但不执行任何任务;false表示添加线程时,根据maximumPoolSize来判断
}
//3:如果执行任务失败,则执行拒绝策略
else if (!addWorker(command, false)) // false表示添加线程时,根据maximumPoolSize来判断
reject(command); // 执行线程创建失败的拒绝策略
}
在execute中为什么要double check 线程池的状态呢?
在多线程环境下,线程池的状态时刻在变化,而ctl.get()是非原子操作,很有可能刚获取了线程池状态后线程池状态就改变了。判断是否将command加入workque是线程池之前的状态。倘若没有double check,万一线程池处于非running状态(在多线程环境下很有可能发生),那么command永远不会执行。
什么是ctl?
在ThreadPoolExecutor中 ctl相关如下:
是通过32位的整形变量ctl通过位运算的方式,保存着线程池的状态以及线程池中线程的数量。【高3位】表示:线程池状态 + 【低29位】表示:线程池中线程数量,如下图所示:
针对ctl.get()获得的int值,有3个重要方法:
-
int runStateOf(int c)获取运行状态 RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED。
-
int workerCountOf(int c)取出低位29位的值,表示获得当前活动的线程数。
-
int ctlOf(int rs, int wc)计算ctl的值,ctl=[3位]线程池状态 + [29位]线程池中线程数量。
1、线程池中的线程数量小于核心线程数代码逻辑
如果当前线程池中的线程数小于核心线程数,则调用addWorker(command,true)创建新的线程来执行提交的command任务,如果该操作成功,则直接返回;如果操作失败,则再次获取ctl的值(包含状态和线程数量),用于后续的操作。
2、任务添加到队列的代码逻辑
- 通过isRunning方法来判断线程池状态是不是运行中状态,如果是,则将command任务放到阻塞队列workQueue中。
- 添加到阻塞队列成功后,还有一些后续操作。比如:再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉,并调用拒绝策略。
- 否则,判断如果当前活动的线程数如果为0,则表明只去创建线程,而此处,并不执行任务(因为,任务已经在上面的offer方法中被添加到了workQueue中了,等待线程池中的线程去消费队列中的任务)
3、线程池中的线程数量小于最大线程数代码逻辑以及拒绝策略的代码逻辑
由于调用addWorker的第二个参数是false,则表示对比的是最大线程数,那么如果往线程池中创建线程依然失败,即addWorker返回false,那么则进入if语句中,直接调用reject方法调用拒绝策略了。
6.4 源码解析------addWorker(Runnable firstTask,boolean core)
addWorker主要负责创建新的线程并执行任务 线程池创建新线程执行任务时,需要 获取全局锁:
执行流程图:
addWorkder的整个流程,其实可以分为两部分:
- 第一部分,试图将workerCount+1。
- 第二部分,workerCount成功+1后,创建Worker(也是一个Runnable),加入集合workers中,并启动Worker线程。
addWorker源码:
java
/**
* @param firstTask 需要执行的Runnable线程
* @param core true:新增线程时,【当前活动的线程数】是否 < corePoolSize
* false:新增线程时,【当前活动的线程数】是否 < maximumPoolSize
*/
private boolean addWorker(Runnable firstTask, boolean core) {
// CAS更新线程池数量
retry:
/** 1:试图将workerCount+1 */
for (;;) {
//获取ctl 并从ctl获取线程池运行状态
int c = ctl.get();
int rs = runStateOf(c); // 获得运行状态runState
/**
* 只有如下两种情况可以返回true而继续执行下去(即:新增worker):
* case 1:rs==RUNNING,表示线程池处于正常的运行状态
* case 2:rs==SHUTDOWN && firstTask==null && !workQueue.isEmpty()
* 表示线程池关闭状态,但是阻塞队列中还有未执行的任务,可以通过firstTask==null启用work来消费阻塞队列中遗留的任务
*/
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c); // 获得当前线程池里的线程数
/**
* 满足如下任意情况,则新增worker失败,返回false
* case1:大于等于最大线程容量,即:int CAPACITY = 00011111111111111111111111111111 = 536870911(十进制)
* case2:当core是true时:>= 核心线程数,当core是false时:>= 最大线程数
*/
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;
// 当前工作线程数成功执行+1操作,则跳出retry标识的这两层for循环
if (compareAndIncrementWorkerCount(c)) break retry;
// 如果线程数加1操作失败,则获取当前最新的线程池运行状态
c = ctl.get();
// 判断线程池运行状态(rs)是否改变;如果不同,则说明方法处理期间线程池运行状态发生了变化,重新获取最新runState
if (runStateOf(c) != rs) continue retry;
}
}
/** 2:workerCount成功+1后,创建Worker,加入集合workers中,并启动Worker线程 */
boolean workerStarted = false; // 用于判断新的worker实例是否已经开始执行Thread.start()
boolean workerAdded = false; // 用于判断新的worker实例是否已经被添加到线程池的workers队列中
Worker w = null;
try {
w = new Worker(firstTask); // 创建Worker实例,每个Worker对象都会针对入参firstTask来创建一个线程。
final Thread t = w.thread; // 从Worker中获得新建的线程t
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); // 添加worker的操作需要加锁
try {
int rs = runStateOf(ctl.get());
/**
* 满足如下任意条件,即可向线程池中添加线程:
* case1:线程池状态为RUNNING
* case2:线程池状态为SHUTDOWN 并且 firstTask等于null。
*/
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
// 因为t是新构建的线程,还没有启动。所以,如果是alive状态,说明已经被启动了,则抛出异常
if (t.isAlive()) throw new IllegalThreadStateException();
workers.add(w); // 向workers集合中保存新创建的work实例
int s = workers.size();
if (s > largestPoolSize) largestPoolSize = s; // largestPoolSize用于记录线程池中曾经存在的最大的线程数量
workerAdded = true; // 向线程池中添加Worker成功
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start(); // 如果向线程池中添加Worker成功,则开启线程
workerStarted = true; // worker启动成功
}
}
} finally {
if (!workerStarted) addWorkerFailed(w); // 如果没有开启线程, 说明往线程池中添加worker失败了
}
return workerStarted;
}
6.5 源码解析------ runWorker(Worker w)
java
/*
1.继承了AQS类,可以方便的实现工作线程的中止操作;
2.实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;
3.当前提交的任务firstTask作为参数传入Worker的构造方法;
*/
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
private static final long serialVersionUID = 6138294804551838833L;
final Thread thread; // 真正在线程池中运行的线程
Runnable firstTask; // 线程运行的任务
volatile long completedTasks; // 执行完毕的任务数量
Worker(Runnable firstTask) {
setState(-1); // AQS.state=-1
this.firstTask = firstTask; // 待Worker调用的任务
this.thread = getThreadFactory().newThread(this); // 通过ThreadFactory构建线程,生成运行work任务的线程
}
/** 开启线程执行操作 */
public void run() {
runWorker(this);
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null); // 清空ownerThread
setState(0); // state=0
return true;
}
//......
}
runWorker()方法是线程池的核心,执行流程如下:
线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行可中断;
Worker执行firstTask或从workQueue中获取任务:
- 进行加锁操作,保证thread不被其他线程中断(除非线程池被中断)
- 检查线程池状态,倘若线程池处于中断状态,当前线程将中断。
- 执行beforeExecute
- 执行任务的run方法
- 执行afterExecute方法
- 解锁操作
java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // 获得Worker待运行的任务task
w.firstTask = null; // help GC
// 将AQS锁资源的状态由-1变成0,因为在创建的时候将state设为-1了,现在开始执行任务了,也就需要加锁了,所以要把state再重新变为0,这样在
// 后面执行任务的时候才能用来加锁,保证任务在执行过程中不会出现并发异常解锁
w.unlock();
boolean completedAbruptly = true; // 用来判断执行任务的过程中,是否出现了异常
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// 如果当前线程池正在停止(STOP、TIDYING、TERMINATED),如果wt被调用了interrupt(),则不做处理;如果没有被调用,则调用interrupt()
if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt(); /** 为当前线程添加中断状态 */
try {
beforeExecute(wt, task); // doing Nothing
Throwable thrown = null;
try {
task.run(); /** 真正执行任务的地方 */
} catch (RuntimeException x) {
thrown = x;
throw x;
} catch (Error x) {
thrown = x;
throw x;
} catch (Throwable x) {
thrown = x;
throw new Error(x);
} finally {
afterExecute(task, thrown); // doing Nothing
}
} finally {
task = null; // help GC
w.completedTasks++; // 完成任务数+1
w.unlock(); // 解锁
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly); // 无论什么情况导致Worker跳出自旋,则执行该方法
}
}
6.6 源码解析------ getTask()
通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
java
private Runnable getTask() {
boolean timedOut = false; // 表示上次从阻塞队列中获取任务是否超时
for (; ; ) {
int c = ctl.get();
int rs = runStateOf(c);
/**
* 同时满足如下两点,则线程池中工作线程数减1,并返回null
* 1> rs >= SHUTDOWN,表示线程池不是RUNNING状态
* 2> rs >= STOP 表示STOP、TIDYING和TERMINATED这三个状态,它们共同点就是【不接收新任务】也【不处理workQueue里的线程任务】or 阻塞队列workQueue为空
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount(); // 线程池中工作线程数-1
return null;
}
int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // timed用于判断是否需要进行超时控制
/**
* 同时满足以下两种情况,则线程池中工作线程数减1并返回null:
* case1:当前活动线程数workCount大于最大线程数,或者需要超时控制(timed=true)并且上次从阻塞队列中获取任务发生了超时(timedOut=true)
* case2:如果有效线程数大于1,或者阻塞队列为空。
*/
if ((wc > maximumPoolSize || (timed && timedOut)) &&
(wc > 1 || workQueue.isEmpty())) { // 被执行了setMaximumPoolSize
if (compareAndDecrementWorkerCount(c)) return null; // 线程池中工作线程数-1
continue; // 如果减1失败,则循环重试
}
try {
/**
* poll(time):取走BlockingQueue里排在首位的元素,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
* take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻塞进入等待状态直到Blocking有新的对象被加入为止
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : /** poll -->若队列为空,返回null */
workQueue.take(); /** take --> 若队列为空,发生阻塞,等待元素 */
if (r != null) return r; // 获取到阻塞队列中待执行的任务,将其返回
timedOut = true; // 如果没从阻塞队列中取到待运行的任务,即:r=null,表示超时了,标记为上一次超时状态
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
七、调度器的钩子方法
ThreadPoolExecutor线程池调度器为每个任务执行前后都提供了钩子方法。ThreadPoolExecutor类提供了三个钩子方法(空方法),这三个空方法一般用作被子类重写,具体如下:
Java
//任务执行之前的钩子方法
protected void beforeExecute(Thread t, Runnable r) {
}
//任务执行之后的钩子方法
protected void afterExecute(Runnable r, Throwable t) {
}
//线程池终止时的钩子方法
protected void terminated() {
}
三个方法说明如下:
为线程池定制钩子方法的示例:
Java
@org.junit.Test
public void testHooks() {
ExecutorService pool = new ThreadPoolExecutor(2,
4, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
@Override
protected void terminated() {
Print.tco("调度器已经终止!");
}
@Override
protected void beforeExecute(Thread t, Runnable target) {
Print.tco(target + "前钩子被执行");
//记录开始执行时间
START_TIME.set(System.currentTimeMillis());
super.beforeExecute(t, target);
}
@Override
protected void afterExecute(Runnable target, Throwable t) {
super.afterExecute(target, t);
//计算执行时长
long time = (System.currentTimeMillis() - START_TIME.get());
Print.tco(target + " 后钩子被执行, 任务执行时长(ms):" + time);
//清空本地变量
START_TIME.remove();
}
};
pool.execute(new TargetTask());
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}
八、线程池的状态
线程池总共存在5种状态,定义在ThreadPoolExecutor类种
Java
public class ThreadPoolExecutor extends AbstractExecutorService {
// 通过ctl,可以获得当前线程池的运行状态(runStatus)和所包含的线程数量(workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 32 - 3 = 29位,表示用29位来表示线程数量
private static final int COUNT_BITS = Integer.SIZE - 3;
// 0001左移动29位后,减1,则为从低位到高位,连续29位都位1,防止超出29位,表示一个线程池最多能够创建的线程数量00011111111111111111111111111111(转换为10进制为536870911)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static final int RUNNING = -1 << COUNT_BITS;// 1110 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN = 0 << COUNT_BITS;// 0000 0000 0000 0000 0000 0000 0000 0000
private static final int STOP = 1 << COUNT_BITS; // 0010 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING = 2 << COUNT_BITS; // 0100 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED = 3 << COUNT_BITS;// 0110 0000 0000 0000 0000 0000 0000 0000
/**
* 取出高3位的值,表示获取运行状态(RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED)
*/
private static int runStateOf(int c) {
return c & ~CAPACITY; // ~CAPACITY = 11100000000000000000000000000000
}
/**
* 取出低29位的值,表示获得当前活动的线程数
*/
private static int workerCountOf(int c) {
return c & CAPACITY; // CAPACITY = 00011111111111111111111111111111
}
/**
* 计算ctl的值(拼接runState和workerCount)
* ctl=[3位]线程池状态 + [29位]线程池中线程数量
*/
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
// ....
}
ctl初始化了线程状态和线程数量,初始状态为RUNNING,线程数量为0。
java
// 通过ctl,可以获得当前线程池的运行状态(runStatus)和所包含的线程数量(workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
原理很简单,这里用到了位运算。一个int类型的数会占用4个字节,也就是32位。线程池采用一个32位的整数来存储线程状态和线程数量,其中高3位表示线程状态,低29位表示线程数量。
线程池的5种状态具体如下:
- RUNNING,运行状态,可以接收新的任务并处理,可以处理阻塞队列中的任务。
- SHUTDOWN,关闭状态,不接收新的任务,但是可以继续处理阻塞队列中的任务。
- STOP,停止状态,不接收新的任务,不处理阻塞队列中的任务,同时会中断正在处理的任务。
- TIDYING,过渡状态,该状态意味着所有的任务都执行完了,并且线程池中已经没有有效的工作线程。该状态下会调用terminated()方法进入TERMINATED状态。
- TERMINATED,终止状态,terminated()方法调用完成以后的状态。
线程池的状态转换规则为:
- 线程池创建之后状态为RUNNING。
- 执行线程池的shutdown()方法,会使线程池状态从RUNNING转变为SHUTDOWN。
- 执行线程池的shutdownNow()方法,会使线程池状态从RUNNING转变为STOP。
- 当线程池的状态处于SHUTDOWN状态,执行其shutdown()方法会将其状态转变成STOP。
- 等待线程池的所有工作线程停止,工作队列清空之后,线程池的状态会从STOP转换为TIDYING。
- 执行完terminated()钩子方法之后,线程池的状态从TIDYING转变为TERMINATED。
线程池中状态流转图如下:
九、关闭线程池
线程池的关闭主要涉及到以下3个方法:
9.1 shutdown()方法原理
Java
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
//加锁
mainLock.lock();
try {
// 检查权限
checkShutdownAccess();
//设置线程池状态 为SHUTDOWN,在这之后线程池不再接收新提交的任务,此时如果还继续往线程池
//提交任务,将会使用线程池拒绝策略响应,默认的拒绝策略将会使用ThreadPoolExecutor.AbortPolicy,接收新任务时会抛出RejectedExecutionException异常。
advanceRunState(SHUTDOWN);
// 中断空闲线程
interruptIdleWorkers();
//钩子函数,清理一些资源
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
9.2 shutdownNow()方法的原理
Java
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
// 加锁
mainLock.lock();
try {
//检查权限
checkShutdownAccess();
//将线程池状态设置为STOP
advanceRunState(STOP);
//中断所有线程,包括空闲线程以及工作线程,中断线程并不代表线程立刻结束,只是通过工作线程的interrupt()实例方法设置了中断状态,这里需要用户程序主动配合线程进行中断操作。
interruptWorkers();
//丢弃阻塞队列中的所有任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
// 返回所有未执行的任务
return tasks;
}
9.3 awaitTermination()方法的使用
调用了线程池shutdown()与shutdownNow()方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等到线程池关闭完成,需要调用awaitTermination()进行主动等待。调用方法大致如下:
Java
threadPool.shutdown();
try {
//一直等待,直到线程池完成关闭
while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
System.out.println("线程池任务还未执行结束");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
9.4 优雅地关闭线程池
可以结合shutdown、shutdownNow、awaitTermination三个方法去优雅的关闭一个线程池,具体步骤如下:
- 执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
- 执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
- 如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
- 补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。
Java
public static void shutdownThreadPoolGracefully(ExecutorService threadPool) {
if (!(threadPool instanceof ExecutorService) || threadPool.isTerminated()) {
return;
}
try {
threadPool.shutdown(); //第一步:调用shutdown,拒绝接受新任务
} catch (SecurityException e) {
return;
} catch (NullPointerException e) {
return;
}
try {
// 第二步:等待 60 s,等待线程池中的任务完成执行
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 第三步:等待60秒后还没有关闭的话,调用 shutdownNow 取消正在执行的任务
threadPool.shutdownNow();
// 第四步:再次等待 60 s,如果还未结束,可以再次尝试,或则直接放弃
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池任务未正常执行结束");
}
}
} catch (InterruptedException ie) {
// 捕获异常,重新调用 shutdownNow
threadPool.shutdownNow();
}
//第五步:任然没有关闭,循环关闭1000次,每次等待10毫秒
if (!threadPool.isTerminated()) {
try {
for (int i = 0; i < 1000; i++) {
if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {
break;
}
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
System.err.println(e.getMessage());
} catch (Throwable e) {
System.err.println(e.getMessage());
}
}
}
9.5 注册 JVM 钩子函数自动关闭线程池
如果使用了线程池,可以在JVM注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动将线程池优雅关闭,以确保资源正常释放。
Java
//懒汉式单例创建线程池:用于定时任务、顺序排队执行任务
@Slf4j
public class SeqOrScheduledTargetThreadPoolLazyHolder {
//线程池:用于定时任务、顺序排队执行任务
static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(
1,
new CustomThreadFactory("seq"));
public static ScheduledThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("定时和顺序任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
十、确定线程池的线程数
使用线程池的好处:
10.1 按照任务类型对线程池进行分类
使用标准构造器ThreadPoolExecutor创建线程池时,会涉及线程数的配置,而线程数的配置与异步任务类型是分不开的。这里将线程池的异步任务大致分为以下三类:
10.2 为 IO 密集型任务确定线程数
由于IO密集型任务的CPU使用率较低,导致线程空余时间很多,因此通常需要开CPU核心数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。
Java
@Slf4j
//懒汉式单例创建线程池:用于IO密集型任务
public class IoIntenseTargetThreadPoolLazyHolder {
// CPU核数
public static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// IO线程池最大线程数
public static final int IO_MAX = Math.max(2, CPU_COUNT * 2);
//线程池: 用于IO密集型任务
public static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
IO_MAX,
IO_MAX,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new ThreadUtil.CustomThreadFactory("io"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
//设置keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程,当池中的线程长时间空闲时,
//可以自行销毁。
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("IO密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
10.3 为 CPU 密集型任务确定线程数
CPU密集型任务也叫计算密集型任务,其特点是要进行大量计算而需要消耗CPU资源,比如计算圆周率、对视频进行高清解码等。CPU密集型任务虽然也可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用CPU,CPU密集型任务并行执行的数量应当等于CPU的核心数。
比如说4个核心的CPU,通过4个线程并行执行4个CPU密集型任务,此时的效率是最高的。但是如果线程数远远超出CPU核心数量,就需要频繁地切换线程,线程上下文切换时需要消耗时间,反而会使得任务效率下降。因此,对于CPU密集型的任务来说,线程数等于CPU数就行。
java
public class CpuIntenseTargetThreadPoolLazyHolder {
// CPU核数
public static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
public static final int MAXIMUM_POOL_SIZE = CPU_COUNT;
// 线程空闲时间
public static final int KEEP_ALIVE_SECONDS = 30;
//线程池: 用于CPU密集型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
MAXIMUM_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("cpu"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("CPU密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
10.4 为混合型任务确定线程数
混合型任务既要执行逻辑计算,又要进行大量非CPU耗时操作(如RPC调用、数据库访问、网络通信等),所以混合型任务CPU利用率不是太高,非CPU耗时往往是CPU耗时的数倍。比如在Web应用处理HTTP请求处理时,一次请求处理会包括DB操作、RPC操作、缓存操作等多种耗时操作。
Java
//懒汉式单例创建线程池:用于混合型任务
public class MixedTargetThreadPoolLazyHolder {
//首先从环境变量 mixed.thread.amount 中获取预先配置的线程数
//如果没有对 mixed.thread.amount 做配置,则使用常量 MIXED_MAX 作为线程数
private static final int max = (null != System.getProperty(MIXED_THREAD_AMOUNT)) ?
Integer.parseInt(System.getProperty(MIXED_THREAD_AMOUNT)) : MIXED_MAX;
//线程池: 用于混合型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
max,
max,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("mixed"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread("混合型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
十一、源码解析-ThreadPoolExecutor
11.1 关键属性
Java
// 通过ctl,可以获得当前线程池的运行状态(runStatus)和所包含的线程数量(workerCount)
//这个属性是用来存放 当前运行的worker数量以及线程池状态的
//int是32位的,这里把int的高3位拿来充当线程池状态的标志位,后29位拿来充当当前运行worker的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 32 - 3 = 29位
private static final int COUNT_BITS = Integer.SIZE - 3;
//存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
//worker的集合,用set来存放
private final HashSet<Worker> workers = new HashSet<Worker>();
//历史达到的worker数最大值
private int largestPoolSize;
//当队列满了并且worker的数量达到maxSize的时候,执行具体的拒绝策略
private volatile RejectedExecutionHandler handler;
//超出coreSize的worker的生存时间
private volatile long keepAliveTime;
//常驻worker的数量 核心线程数
private volatile int corePoolSize;
//最大worker的数量,一般当workQueue满了才会用到这个参数 最大线程数
private volatile int maximumPoolSize;
11.2 内部状态
Java
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
11.3 任务执行
Java
public void execute(Runnable command) --->
(1.如果线程数小于核心线程数,则创建新线程执行提交的command任务
2.如果当前线程数大于等于核心线程数小于最大线程数,则把command放入到任务阻塞队列中
3.如果阻塞队列已经放满,则创建非核心线程执行从队列对头获取的任务
4.如果队列已满且线程数大于等于最大线程数,则执行拒绝策略
)
private boolean addWorker(Runnable firstTask, boolean core)--->
final void runWorker(Worker w)--->
private Runnable getTask();
11.4 任务提交
AbstractExecutorService.submit()实现了ExecutorService.submit() 可以获取执行完的返回值, 而ThreadPoolExecutor 是AbstractExecutorService.submit()的子类,所以submit方法也是ThreadPoolExecutor`的方法。
Java
// submit()在ExecutorService中的定义
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
java
// submit方法在AbstractExecutorService中的实现
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 通过Executor.execute方法提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;
execute(ftask);
return ftask;
}
11.5 任务关闭
Java
//shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完
public void shutdown();
//shutdownNow做的比较绝,它先将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,然后清空任务队列。
public List<Runnable> shutdownNow();
十二、源码解析-ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor,为任务提供延迟或周期执行,属于线程池的一种。和 ThreadPoolExecutor 相比,它还具有以下几种特性:
- 使用专门的任务类型---ScheduledFutureTask 来执行周期任务,也可以接收不需要时间调度的任务(这些任务通过 ExecutorService 来执行)。
- 使用专门的存储队列---DelayedWorkQueue 来存储任务,DelayedWorkQueue 是无界延迟队列DelayQueue 的一种。相比ThreadPoolExecutor也简化了执行机制
- 支持可选的run-after-shutdown参数,在池被关闭(shutdown)之后支持可选的逻辑来决定是否继续运行周期或延迟任务。并且当任务(重新)提交操作与 shutdown 操作重叠时,复查逻辑也不相同。
12.1 ScheduledThreadPoolExecutor类结构图
12.2 ScheduledThreadPoolExecutor 核心属性
Java
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
//线程池关闭后继续执行已经存在的周期任务,用来控制池关闭之后的任务执行逻辑。
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
//关闭后继续执行已经存在的延时任务,用来控制池关闭之后的任务执行逻辑。
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
//取消任务后移除
//用来控制任务取消后是否从队列中移除。当一个已经提交的周期或延迟任务在运行之前被取消,那么它之后将不会运行。默认配置下,这种已经取消的任务在届期之前不会被移除。 通过这种机制,可以方便检查和监控线程池状态,但也可能导致已经取消的任务无限滞留。为了避免这种情况的发生,我们可以通过setRemoveOnCancelPolicy方法设置移除策略,把参数removeOnCancel设为true可以在任务取消后立即从队列中移除。
private volatile boolean removeOnCancel = false;
//为相同延时的任务提供的顺序编号,保证任务之间的FIFO顺序.与 ScheduledFutureTask 内部的sequenceNumber参数作用一致。
private static final AtomicLong sequencer = new AtomicLong();
}
12.3 ScheduledThreadPoolExecutor构造方法
Java
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 通过super调用了ThreadPoolExecutor的构造函数,并且使用特定等待队列DelayedWorkQueue。
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
12.4 ScheduledThreadPoolExecutor核心方法
1、schedule()方法
Java
//schedule主要用于执行一次性(延迟)任务。
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
/*
1.首先通过triggerTime计算任务的延迟执行时间
2.然后通过 ScheduledFutureTask 的构造函数把 Runnable/Callable 任务构造为ScheduledThreadPoolExecutor可以执行的任务类型
3.最后调用decorateTask方法执行用户自定义的逻辑;
*/
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
// 执行任务
delayedExecute(t);
return t;
}
//decorateTask是一个用户可自定义扩展的方法,默认实现下直接返回封装的RunnableScheduledFuture任务
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
//执行任务
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
//线程池已经关闭,执行拒绝策略
reject(task);
else {
//任务进入阻塞队列
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) && //判断run-after-shutdown参数
remove(task)) //移除任务
task.cancel(false);
else
ensurePrestart(); //启动一个新的线程等待任务
}
}
//池正在运行,或者 run-after-shutdown 参数值为true,则调用父类方法ensurePrestart启动一个新的线程等待执行任务
// ThreadPoolExecutor.java
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
2、 scheduleAtFixedRate 和 scheduleWithFixedDelay
java
/**
* 创建一个周期执行的任务,第一次执行延期时间为initialDelay,
* 之后每隔period执行一次,不等待第一次执行完成就开始计时
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
//构建RunnableScheduledFuture任务类型
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),//计算任务的延迟时间
unit.toNanos(period));//计算任务的执行周期
RunnableScheduledFuture<Void> t = decorateTask(command, sft);//执行用户自定义逻辑
sft.outerTask = t;//赋值给outerTask,准备重新入队等待下一次执行
delayedExecute(t);//执行任务
return t;
}
/**
* 创建一个周期执行的任务,第一次执行延期时间为initialDelay,
* 在第一次执行完之后延迟delay后开始下一次执行
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
//构建RunnableScheduledFuture任务类型
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),//计算任务的延迟时间
unit.toNanos(-delay));//计算任务的执行周期
RunnableScheduledFuture<Void> t = decorateTask(command, sft);//执行用户自定义逻辑
sft.outerTask = t;//赋值给outerTask,准备重新入队等待下一次执行
delayedExecute(t);//执行任务
return t;
}
3、shutdown()
java
public void shutdown() {
super.shutdown();
}
//ThreadPoolExecutor.java
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
//取消并清除由于关闭策略不应该运行的所有任务
@Override void onShutdown() {
BlockingQueue<Runnable> q = super.getQueue();
//获取run-after-shutdown参数
boolean keepDelayed =
getExecuteExistingDelayedTasksAfterShutdownPolicy();
boolean keepPeriodic =
getContinueExistingPeriodicTasksAfterShutdownPolicy();
if (!keepDelayed && !keepPeriodic) {//池关闭后不保留任务
//依次取消任务
for (Object e : q.toArray())
if (e instanceof RunnableScheduledFuture<?>)
((RunnableScheduledFuture<?>) e).cancel(false);
q.clear();//清除等待队列
}
else {//池关闭后保留任务
// Traverse snapshot to avoid iterator exceptions
//遍历快照以避免迭代器异常
for (Object e : q.toArray()) {
if (e instanceof RunnableScheduledFuture) {
RunnableScheduledFuture<?> t =
(RunnableScheduledFuture<?>)e;
if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
t.isCancelled()) { // also remove if already cancelled
//如果任务已经取消,移除队列中的任务
if (q.remove(t))
t.cancel(false);
}
}
}
}
tryTerminate(); //终止线程池
}
12.5 ScheduledThreadPoolExecutor内部类ScheduledFutureTask
java
//继承了FutureTask,说明是一个异步运算任务;最上层分别实现了Runnable、Future、Delayed接口,说明它是一个可以延迟执行的异步运算任务。
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
//核心属性
//为相同延时任务提供的顺序编号
private final long sequenceNumber;
//任务可以执行的时间,纳秒级,通过triggerTime方法计算得出
private long time;
//重复任务的执行周期时间,纳秒级。正数表示固定速率执行(为scheduleAtFixedRate提供服务),负数表示固定延迟执行(为scheduleWithFixedDelay提供服务),0表示不重复任务。
private final long period;
//重新入队的任务 通过reExecutePeriodic方法入队重新排序。
RunnableScheduledFuture<V> outerTask = this;
//延迟队列的索引,以支持更快的取消操作
int heapIndex;
// 核心方法
public void run() {
boolean periodic = isPeriodic();//是否为周期任务
if (!canRunInCurrentRunState(periodic))//当前状态是否可以执行
cancel(false);
else if (!periodic)
//不是周期任务,直接执行
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();//设置下一次运行时间
reExecutePeriodic(outerTask);//重排序一个周期任务
}
}
//设置下一次执行任务的时间
private void setNextRunTime() {
long p = period;
if (p > 0) //固定速率执行,scheduleAtFixedRate
time += p;
else
time = triggerTime(-p); //固定延迟执行,scheduleWithFixedDelay
}
//计算固定延迟任务的执行时间
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}
//重排序一个周期任务
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {//池关闭后可继续执行
super.getQueue().add(task);//任务入列
//重新检查run-after-shutdown参数,如果不能继续运行就移除队列任务,并取消任务的执行
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
ensurePrestart();//启动一个新的线程等待任务
}
}
//cheduledFutureTask.cancel本质上由其父类 FutureTask.cancel 实现
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = super.cancel(mayInterruptIfRunning);
if (cancelled && removeOnCancel && heapIndex >= 0)
//取消任务成功后会根据removeOnCancel参数决定是否从队列中移除此任务
remove(this);
return cancelled;
}
}