目录
一、简介
前面一篇文章我们介绍了各种线程池的使用、线程池的优势等,本篇我们将介绍一下线程池底层工作原理相关知识。
二、线程池底层原理
Executor为我们提供了功能各异的线程池,其实其内部很多都是由ThreadPoolExecutor实现的,了解ThreadPoolExecutor实现原理,不但对我们使用理解Executor提供的线程池大有帮助,也让我们能根据实际情况自定义特定的线程池。
我们先看看几种常用的线程池的方法,可以看到,基本上全是通过ThreadPoolExecutor来进行构建的:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
接着看一下ThreadPoolExecutor类的构造方法:
/**
* 使用给定的初始参数和默认线程工厂以及被拒绝的执行处理程序创建一个新的ThreadPoolExecutor。使用executor工厂方法而不是这个通用构造函数可能更方便.
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
//调用本类其他构造方法
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
/**
* 使用给定的初始参数创建一个新的ThreadPoolExecutor.
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//可见maximumPoolSize 参数值必须大于等于corePoolSize
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到ThreadPoolExecutor的构造方法中有七个参数,下面对这七大参数做一下说明:
- 【a】corePoolSize:线程池中的常驻核心线程的数量(总线程量可大于等于这个值);
- 【b】maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1(总线程量不可能超越这个数值);
- 【c】keepAliveTime:多余的空闲线程的存活时间。当线程池中线程的数量超过corePoolSize时,并且当其空闲时间达到keepAliveTime时,多余的线程会被销毁直到只剩下corePoolSize个线程为止;
- 【d】unit:keepAliveTime的时间单位;
- 【e】workQueue:任务队列,被提交但尚未被执行的任务;例如: 提交了10个任务,但是线程只有5个,于是另外5个提交但没开始执行的任务就存放到了workQueue工作队列里面,既然是队列,我们知道,实现队列的方式有很多种,比如ArrayBlockQueue、LinkedBlockQueue等等,选择不同的队列就会带来不同的问题;
- ArrayBlockQueue:当队列中的任务过多时,有可能超出队列长度;
- LinkedBlockQueue:接受过多的任务可能会占用太多内存,造成内存崩溃;
- 【f】threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般使用默认的线程工厂即可;
- 【g】handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝请求的策略。默认拒绝策略是AbortPolicy,直接抛出异常;
三、自定义线程池
在实际项目中,不太建议直接使用Executors工具类的方式来创建线程池,推荐根据项目具体需求自己创建线程池,动态配置ThreadPoolExecutor所需要的参数。自定义线程池直接通过ThreadPoolExecutor一个实例对象即可,下面我们通过一个简单的示例说明如何自定义线程池。
public class T14_ThreadPool {
public static void main(String[] args) {
ExecutorService executorService = getExecutorService();
//提交十个任务
for (int i = 0; i < 10; i++) {
final int num = i;
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======获取线程======, i = " + num));
}
executorService.shutdown();
}
private static ExecutorService getExecutorService() {
return new ThreadPoolExecutor(
5, //常驻核心线程的数量
5, //线程池中能够容纳同时执行的最大线程数
0L, //多余的空闲线程的存活时间
TimeUnit.MILLISECONDS, //时间单位:毫秒
new LinkedBlockingQueue<>(5), //阻塞队列
Executors.defaultThreadFactory(), //默认线程池生产工厂
new ThreadPoolExecutor.AbortPolicy() //拒绝策略
);
}
}
运行结果:
pool-1-thread-1=======获取线程======, i = 0
pool-1-thread-1=======获取线程======, i = 5
pool-1-thread-1=======获取线程======, i = 6
pool-1-thread-2=======获取线程======, i = 1
pool-1-thread-2=======获取线程======, i = 8
pool-1-thread-2=======获取线程======, i = 9
pool-1-thread-1=======获取线程======, i = 7
pool-1-thread-4=======获取线程======, i = 3
pool-1-thread-5=======获取线程======, i = 4
pool-1-thread-3=======获取线程======, i = 2
可见,提交了十个任务,并且十个任务都成功执行了。下面我们提交十一个任务试一下:
//提交十一个任务
for (int i = 0; i < 11; i++) {
final int num = i;
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======获取线程======, i = " + num));
}
运行结果:
pool-1-thread-1=======获取线程======, i = 0
pool-1-thread-2=======获取线程======, i = 1
pool-1-thread-1=======获取线程======, i = 5
pool-1-thread-2=======获取线程======, i = 6
pool-1-thread-1=======获取线程======, i = 7
pool-1-thread-1=======获取线程======, i = 9
pool-1-thread-2=======获取线程======, i = 8
pool-1-thread-3=======获取线程======, i = 2
java.util.concurrent.RejectedExecutionException: Task juc03.T14_ThreadPool$$Lambda$1/1078694789@7699a589 rejected from java.util.concurrent.ThreadPoolExecutor@58372a00[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at juc03.T14_ThreadPool.main(T14_ThreadPool.java:12)
pool-1-thread-4=======获取线程======, i = 3
pool-1-thread-5=======获取线程======, i = 4
可见,当我们提交的任务数量大于【队列长度 + 最大线程数】时,由于我们使用的AbortPolicy拒绝策略,直接报错,报错如上图所示。
通过new ThreadPoolExecutor()我们就可以根据具体的业务场景很灵活地创建线程池,但是实际项目中corePoolSize 参数一般跟我们电脑处理器的数量挂钩,然后workQueue工作队列也建议使用有界阻塞队列,避免任务太多,创建太多的线程。handler参数的话就需要根据具体需求来定了,下面我们也会单独对四种拒绝策略做一个介绍。
下面给出一个参考的线程池创建方式:
private static ExecutorService getExecutorService() {
//常驻核心线程的数量 : 最大可用的处理器数量 * 2
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
//创建有界阻塞队列
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(256);
//拒绝策略
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.DiscardPolicy();
return new ThreadPoolExecutor(
corePoolSize, //常驻核心线程的数量
corePoolSize, //线程池中能够容纳同时执行的最大线程数
0L, //多余的空闲线程的存活时间
TimeUnit.MILLISECONDS, //时间单位:毫秒
workQueue, //阻塞队列
Executors.defaultThreadFactory(), //默认线程池生产工厂
rejectedExecutionHandler //拒绝策略
);
}
四、线程池拒绝策略
ThreadPoolExecutor类中存在四个私有静态内部类AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy,这四个类就是它提供的四种拒绝策略相关类。
线程池给我们提供了几种常见的拒绝策略:
下面对各种拒绝策略做一个介绍:
拒绝策略 | 拒绝行为 |
AbortPolicy | 默认的拒绝策略,当提交的任务数量大于线程池中的最大数量时,会抛出RejectedExecutionException,阻止系统正常运行。 |
DiscardPolicy | 该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略(什么也不做,直接忽略)。 |
DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中,尝试再次提交当前任务(丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置)。 |
CallerRunsPolicy | “调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量(直接由提交任务者执行这个任务)。 |
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非检查时异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被丢弃。
下面通过示例了解各种拒绝策略的使用:
【a】CallerRunsPolicy
public class T14_ThreadPool {
public static void main(String[] args) {
ExecutorService executorService = getExecutorService();
//提交十一个任务
for (int i = 0; i < 11; i++) {
final int num = i;
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======获取线程======, i = " + num));
}
executorService.shutdown();
}
private static ExecutorService getExecutorService() {
return new ThreadPoolExecutor(
5, //常驻核心线程的数量
5, //线程池中能够容纳同时执行的最大线程数
0L, //多余的空闲线程的存活时间
TimeUnit.MILLISECONDS, //时间单位:毫秒
new LinkedBlockingQueue<>(5), //阻塞队列
Executors.defaultThreadFactory(), //默认线程池生产工厂
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);
}
}
运行结果:
main=======获取线程======, i = 10
pool-1-thread-1=======获取线程======, i = 0
pool-1-thread-1=======获取线程======, i = 5
pool-1-thread-2=======获取线程======, i = 1
pool-1-thread-1=======获取线程======, i = 6
pool-1-thread-3=======获取线程======, i = 2
pool-1-thread-2=======获取线程======, i = 7
pool-1-thread-3=======获取线程======, i = 9
pool-1-thread-1=======获取线程======, i = 8
pool-1-thread-4=======获取线程======, i = 3
pool-1-thread-5=======获取线程======, i = 4
可见,多余的一个任务被任务的调用者main线程执行了。
【b】DiscardOldestPolicy
public class T14_ThreadPool {
public static void main(String[] args) {
ExecutorService executorService = getExecutorService();
//提交十一个任务
for (int i = 0; i < 11; i++) {
final int num = i;
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======获取线程======, i = " + num));
}
executorService.shutdown();
}
private static ExecutorService getExecutorService() {
return new ThreadPoolExecutor(
5, //常驻核心线程的数量
5, //线程池中能够容纳同时执行的最大线程数
0L, //多余的空闲线程的存活时间
TimeUnit.MILLISECONDS, //时间单位:毫秒
new LinkedBlockingQueue<>(5), //阻塞队列
Executors.defaultThreadFactory(), //默认线程池生产工厂
new ThreadPoolExecutor.DiscardOldestPolicy() //拒绝策略
);
}
}
运行结果:
pool-1-thread-1=======获取线程======, i = 0
pool-1-thread-2=======获取线程======, i = 1
pool-1-thread-3=======获取线程======, i = 2
pool-1-thread-3=======获取线程======, i = 8
pool-1-thread-3=======获取线程======, i = 9
pool-1-thread-1=======获取线程======, i = 7
pool-1-thread-2=======获取线程======, i = 6
pool-1-thread-3=======获取线程======, i = 10
pool-1-thread-4=======获取线程======, i = 3
pool-1-thread-5=======获取线程======, i = 4
可见,i = 5那一次是在队列中等待最久的,使用这个策略抛弃等待最久的线程。
【c】DiscardPolicy
public class T14_ThreadPool {
public static void main(String[] args) {
ExecutorService executorService = getExecutorService();
//提交十一个任务
for (int i = 0; i < 11; i++) {
final int num = i;
executorService.execute(() -> System.out.println(Thread.currentThread().getName() + "=======获取线程======, i = " + num));
}
executorService.shutdown();
}
private static ExecutorService getExecutorService() {
return new ThreadPoolExecutor(
5, //常驻核心线程的数量
5, //线程池中能够容纳同时执行的最大线程数
0L, //多余的空闲线程的存活时间
TimeUnit.MILLISECONDS, //时间单位:毫秒
new LinkedBlockingQueue<>(5), //阻塞队列
Executors.defaultThreadFactory(), //默认线程池生产工厂
new ThreadPoolExecutor.DiscardPolicy() //拒绝策略
);
}
}
运行结果:
pool-1-thread-1=======获取线程======, i = 0
pool-1-thread-1=======获取线程======, i = 5
pool-1-thread-1=======获取线程======, i = 6
pool-1-thread-1=======获取线程======, i = 7
pool-1-thread-1=======获取线程======, i = 8
pool-1-thread-1=======获取线程======, i = 9
pool-1-thread-2=======获取线程======, i = 1
pool-1-thread-3=======获取线程======, i = 2
pool-1-thread-4=======获取线程======, i = 3
pool-1-thread-5=======获取线程======, i = 4
可见,i = 10那个任务已经被默默地抛弃掉了。
五、线程池执行流程
线程池的整体执行流程可以用下面的图概括:
文字描述:
任务进来时,首先执行判断,判断常驻核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就创建非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
六、如何配置线程池
根据不同场景配置不同的线程池参数,这是最理想的线程池创建方式,下面给出了几种常用场景下的配置方法参考:
- CPU密集型的任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集型的任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
- 混合型的任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
七、总结
本文总结了线程池的底层实现ThreadPoolExecutor类,并讲解了如何自定义线程池,最后面还总结了几种线程池针对未能正确处理的任务的拒绝策略。在实际项目中,推荐自己自定义线程池,虽然Executors给我们提供了很多方便的工具类,但是自定义线程池可以更加灵活地应对各种场景。任何东西都有两面性,线程池固然有很多优点,但是使用线程池也会存在一定的风险:
用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足,并发错误,线程泄漏,请求过载等等。
参考资料:
https://www.jianshu.com/p/6d941f0ded66
https://blog.csdn.net/weixin_40271838/article/details/79998327