ThreadPoolExecutor解析
前言:在最新的阿里规范中强制使用ThreadPoolExecutor方式创建线程池,不允许使用Executors,因此有必要对ThreadPoolExecutor进行进一步了解。
1.ThreadPoolExecutor介绍
线程池类,直接看其入参最多的构造函数:

参数意义:
corePoolSize
核心线程数的大小。默认情况下,在创建了线程池之后,线程池中的线程数为0,当有任务到来后,如果线程池中存活的线程数小于corePoolSize,则创建一个线程。
maximumPoolSize
线程池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量。当任务数量比corePoolSize大时,任务添加到workQueue,当workQueue满了,将继续创建线程以处理任务。maximumPoolSize表示当wordQueue满了,线程池中最多可以创建的线程数量。
keepAliveTime、unit
当线程池处于空闲状态时,超过keepAliveTime时间之后,空闲的线程会被终止。只有当线程池中的线程数大于corePoolSize时,这个参数才会起作用,但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;当线程数大于corePoolSize时,如果一个线程的空闲时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
workQueue
阻塞队列,存储提交的等待任务。
threadFactory
线程工厂,指定创建线程的工厂
handler
当任务超出线程池范围和队列容量时,采取何种拒绝策略。
对于上述参数,源码注释中有很详细的解释。这里笔者挑出认为重要的几段:

这里表明了corePoolSize、maximumPoolSize和workQueue的关系(上述注释说的非常的清楚,这里稍微翻译下):
#1.默认情况下,线程池初始化的时候,线程数为0。当接收到新任务时,如果线程池中存活的线程数小于corePoolSize,则新建一个线程。
#2.当运行的线程数超出核心线程数时,执行器更多的选择是将任务放入队列中,而不是新建一个线程。
#3.当队列满后,任务不能提交到队列,在不超过maximumPoolSize(最大线程数)的情况下,会创建一个新线程去执行任务,当超过maximumPoolSize时,任务将被拒绝(这里就关联到接下来说要介绍的内容,在任务操作maximumPoolSize时,线程池所使用拒绝策略)。

当执行器关闭、线程池满了、队列满了,则新任务会被拒绝。使用的拒绝策略有以下几种:

注释解释的非常清楚,线程池采用的拒绝策略共有4种:
#1.AbortPolicy : 默认策略,当任务被拒绝时直接抛出异常RejectedExecutionException。
#2.CallerRunsPolicy : 让调用者所在的线程来执行任务,这种策略并不会丢弃任务,但是会降低执行器处理任务的速率。
#3.DiscardPolicy : 直接丢弃新任务。
#4.DiscardOldestPolicy : 如果执行器未关闭,删除队列中第一个任务,再次执行任务。如果失败会重试(repeated)。
接下来看线程池的排队策略。
线程池提供了3种排队的策略:
#1.直接提交(SynchronousQueue):直接提交任务,不保存任务。直接提交策略无容量限制,但是当任务数量过速增长有可能撑爆“JVM”。在生产中一般不采用此策略。

#2.无界队列(LinkedBlockingQueue):当所有核心线程都在忙时,用一个无界队列存放提交的任务。最大线程数设置了也无效。使用无界队列会保存核心线程处理不了的任务,队列无上限,因此最大线程数设置了也无效,无界队列需谨慎使用。

#3.有界队列(ArrayBlockingQueue):用一个有界队列帮助防止资源被耗尽,不过调整和控制比较难。因为队列容量小了,任务不能立即执行,当然需要配合拒绝策略;队列容量太大,又比较耗费资源。当然在生产环境中一般使用有界队列的排队策略,因为使用有界队列可以保存超过核心线程的任务,并且队列有上限,超过上限,新建线程抛错,可以更好的保护资源,防止崩溃。

通过以上分析,可以发现corePoolSize、maximumPoolSize和排队策略是相互影响的,maximumPoolSize的值并不一定有效。
接下来看看线程池的存活机制

当创建的线程超过核心线程数时,线程池会让该线程保持存活keepAliveTime时间,超过该时间后会销毁该线程。默认情况下该值对非核心线程有效,如果想让核心线程也适用于该机制,可以调用allowCoreThreadTimeOut()方法,但是这样的话就不存在核心线程的概念了。
综合以上,线程池在多次执行任务后,会一直维持部分线程存活,即使它是闲置的。目的是为了减少线程销毁创建的开销,下次有任务需要执行,直接从池子里拿线程就能用了。但核心线程不能维护太多,因为也需要一定开销。最大的线程数保护了整个系统的稳定性,避免并发量大的时候,把线程挤满。工作队列则是保证了任务顺序和暂存,系统的可靠性。线程存活规则的目的和维护核心线程的目的类似,但降低了它的存活的时间。
2.线程状态控制

ctl变量是整个线程池的核心控制状态,它是一个AtomicInteger类型的原子对象,它记录了线程池中生效线程数和线程池的运行状态。
- workerCount,生效的线程数,基本上可以理解为存活的线程数。
- runState,线程池运行状态。
ctl总共32位,其中低29位代表workerCount,所以最大线程数为(2^29)-1。高3位代表runState。
runState有5个值:

各值对应的值如下:
RUNNING -- 对应的高3位值是111。
SHUTDOWN -- 对应的高3位值是000。
STOP -- 对应的高3位值是001。
TIDYING -- 对应的高3位值是010。
TERMINATED -- 对应的高3位值是011。
- RUNNING,接收新任务处理队列任务。
- SHUTDOWN,不接收新任务,但处理队列任务。
- STOP,不接收新任务,也不处理队列任务,并且中断所有处理中的任务。
- TIDYING,所有任务都被终结,有效线程为0,并触发terminated()方法。
- TERMINATED,当terminated()方法执行结束。
线程池各个状态之间的切换:

当调用了shutdown(),状态会从RUNNING变成SHUTDOWN,不再接收新任务,此时会处理完队列里面的任务。
如果调用的是shutdownNow(),状态会直接变成STOP。
当线程或者队列都是空的时候,状态就会变成TIDYING。
当terminated()执行完的时候,就会变成TERMINATED。

3.关键函数解析
execute(Runnable)
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
execute函数的主要流程源码中的注释已经讲得非常清楚了。
- 如果少于核心线程在运行,则尝试创建一个新的线程。
- 如果任务成功入队,需再次检查线程池状态看是否需要入队,因为在入队过程中,有可能状态发生变化;如果确认入队但没有存活线程,则新建一个空线程。
- 如果不能入队,则尝试新创建一个线程,如果失败,则拒绝任务。
- 注意在第二步最后会新建一个线程,这里会有一个轮询机制让下个task出队,然后直接利用这个空闲线程。
在execute中我们主要关注addWorker()函数。
首先看下该函数的整体注释了解其大致流程。

- 该函数会检查当前线程池是否可以创建worker(线程)。
- 当线程池stop或者shut down,又或者线程工厂创建线程失败时都会返回false。
- 在线程创建失败时,会进行回滚。
- 注意core参数:true表示以corePoolSize作为参照,false表示以maximumPoolSize为参照。
接下来分析addWorker源码:
private boolean addWorker(Runnable firstTask, boolean core) {
retry: // 标记,表示跳出循环时,从哪里开始执行,类似于goto
for (;;) {
int c = ctl.get(); // 获取ctl对应的值,“生效线程数”和“线程池状态”
int rs = runStateOf(c); // 获取线程池状态
// Check if queue empty only if necessary.
// 如果该if判断想要返回false,队列为空为必要条件,因为addWorker()不只是在接收新任务会调用到,处理队列的任务也会调用到。在线程池状态为SHUTDOWN时还会处理队列中的任务,所以队列不为空会继续向下执行
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
/* 内循环意义:判断worker是否符合corePoolSize和maximumPoolSize定义,不满足则返回false;
然后利用CAS自增workerCount,如果CAS成功则退出循环;
如果CAS失败会继续自旋,在自旋过程中会检查线程池状态,如果发生变化,则回退到外层循环,重新执行。
因此内循环的主要作用就是让workerCount在符合条件下自增。
*/
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
// 这段代码的主要功能:添加任务到线程池,并启动任务所在的线程
try {
// 创建一个Worker对象,包含一个由线程工厂创建的线程和一个需执行的任务
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 线程创建成功 获取一个可重入锁,把Worker对象放入worker成员变量中
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// 检查线程池状态和线程状态
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w); // 将Worker变量加入workers中(集合)
// 更新largestPoolSize
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 如果任务添加成功,则启动任务所在的线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 如果任务添加失败则执行addWorkerFailed进行回滚
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
addWorkerFailed(Worker),任务添加失败回滚函数:
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
// 加锁回滚
mainLock.lock();
try {
if (w != null)
workers.remove(w); // 回滚workers
decrementWorkerCount();// 回滚workerCount
tryTerminate();// 判断线程池状态,是否需要终结线程池
} finally {
mainLock.unlock();
}
}
4.总结
ThreadPoolExecutor我们主要关注其addWorker方法,对于其他方法,可翻看源码,比较好理解。
核心要点:
- 当核心线程忙碌时,线程池更倾向于把任务放进队列,而不是新建线程。
- 三种不同的排队策略,根据选择队列的不同,maximumPoolSize不一定有用的。
- ctl是线程池的核心控制状态,包含的runState线程池运行状态和workCount有效线程数。
- retry:是一种标记循环的语法,retry可以是任何变量命名合法字符。
by Shawn Chen,2019.02.16,下午。
ThreadPoolExecutor解析的更多相关文章
- J.U.C ThreadPoolExecutor解析
Java里面线程池顶级接口是Executor,但严格意义上讲Executor并不是一个线程池,而是一个线程执行工具,真正的线程池接口是ExecutorService.关系类图如下: 首先Executo ...
- 含源码解析,深入Java 线程池原理
从池化技术到底层实现,一篇文章带你贯通线程池技术. 1.池化技术简介 在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能. 在编程领域,比较典型的池化技术有: 线程池.连接池.内存池 ...
- Java线程池详解
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- 面试总结——Java篇
前言:前期对Java基础的相关知识点进行了总结,具体参看:Java基础和面试知识点.近期由于笔者正在换工作(ing),因此下面将笔者在面试过程中或笔者朋友面试过程中反馈的题目进行总结,相信弄清楚下面题 ...
- Java线程池详解(一)
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- Java同步之线程池详解
带着问题阅读 1.什么是池化,池化能带来什么好处 2.如何设计一个资源池 3.Java的线程池如何使用,Java提供了哪些内置线程池 4.线程池使用有哪些注意事项 池化技术 池化思想介绍 池化思想是将 ...
- Java 线程池架构原理和源码解析(ThreadPoolExecutor)
在前面介绍JUC的文章中,提到了关于线程池Execotors的创建介绍,在文章:<java之JUC系列-外部Tools>中第一部分有详细的说明,请参阅: 文章中其实说明了外部的使用方式,但 ...
- ThreadPoolExecutor参数解析
ThreadPoolExecutor是一个非常重要的类,用来构建带有线程池的任务执行器,通过配置不同的参数来构造具有不同规格线程池的任务执行器. 写在前面的是: 线程池和任务执行器,线程池的定义比较直 ...
- ThreadPoolExecutor系列<三、ThreadPoolExecutor 源码解析>
本文系作者原创,转载请注明出处:http://www.cnblogs.com/further-further-further/p/7681826.html 在源码解析前,需要先理清线程池控制的运行状态 ...
随机推荐
- mybatis中resultMap配置细则
resultMap算是mybatis映射器中最复杂的一个节点了,能够配置的属性较多,我们在mybatis映射器配置细则这篇博客中已经简单介绍过resultMap的配置了,当时我们介绍了resultMa ...
- ZooKeeper 01 - 什么是ZooKeeper + 部署ZooKeeper集群
目录 1 什么是ZooKeeper 2 ZooKeeper的功能 2.1 配置管理 2.2 命名服务 2.3 分布式锁 2.4 集群管理 3 部署ZooKeeper集群 3.1 下载并解压安装包 3. ...
- ASP.NET Core 2.1 : 十三.httpClient.GetAsync 报SSL错误的问题
不知什么时候 ,出现了这样的一个奇怪问题,简单的httpClient.GetAsync("xxxx")居然报错了.(ASP.NET Core 系列目录) 一.问题描述 把原来的程序 ...
- javaWeb项目中的路径格式 请求url地址 客户端路径 服务端路径 url-pattern 路径 获取资源路径 地址 url
javaweb项目中有很多场景的路径客户端的POST/GET请求,服务器的请求转发,资源获取需要设置路径等这些路径表达的含义都有不同,所以想要更好的书写规范有用的路径代码 需要对路径有一个清晰地认知 ...
- GAN模型生成手写字
概述:在前期的文章中,我们用TensorFlow完成了对手写数字的识别,得到了94.09%的识别准确度,效果还算不错.在这篇文章中,笔者将带领大家用GAN模型,生成我们想要的手写数字. GAN简介 对 ...
- Perl信号处理
本文关于Perl信号处理的内容主体来自于<Pro Perl>的第21章. 信号处理 操作系统可以通过信号(signal)处理机制来实现一些功能:程序注册好待监视的信号处理机制,在程序运行过 ...
- Scala(三)
一.控制语句 var x = 40 if(x == 40){ println("greate") } 二.循环 (1) 一般循环 while(a>1){ if(a==2){ ...
- 为你的Python程序加密
在实际的工作中,有时候我们需要部署自己的Python应用,但这时候我们并不希望别人能够看到自己的Python源程序.因此,我们需要为自己的源代码进行加密,Python已经为我们提供了这样一套工作机 ...
- 第9章 使用客户端凭据保护API - Identity Server 4 中文文档(v1.0.0)
快速入门介绍了使用IdentityServer保护API的最基本方案. 我们将定义一个API和一个想要访问它的客户端. 客户端将通过提供ClientCredentials在IdentityServer ...
- C#多线程编程的同步也线程安全
前一篇文章记录了简单的多线程编程的几种方式,但是在实际的项目中,也需要等待多线程执行完成之后再执行的方法,这个就叫做多线程的同步,或者,由于多个线程对同一对象的同时操作造成数据错乱,需要线程安全.这篇 ...