Java 线程池源码解读与实践
肖文英
Java 资深研发工程师
27 人已学习
立即订阅
课程目录
已更新 28 讲/共 32 讲
并发编程基础知识 (8讲)
时长 09:08
时长 40:33
时长 21:26
时长 44:37
时长 09:11
线程池基础知识 (6讲)
时长 12:38
时长 09:28
时长 30:59
时长 44:51
时长 45:02
时长 15:15
线程池实现详解 (8讲)
时长 14:10
时长 23:21
时长 25:00
时长 13:57
时长 36:02
时长 17:28
时长 42:08
常见开源线程池 (3讲)
时长 22:14
时长 27:06
时长 25:32
Java 线程池源码解读与实践
15
15
1.0x
00:00/00:00
登录|注册

Tomcat线程池

1 池化技术

池化技术是一种常用的提升性能的手段,比如常见的数据库连接池、JAVA 字符串的常量池、以及线程池等。
以 JAVA 中的线程为例,创建一个新线程的背后,其实是调用操作系统的 api,消耗宝贵的系统资源,去执行一些业务逻辑。如果我们频繁的创建销毁线程对象,对于 CPU 和内存都是一个很大的负担。所以,线程池就应运而生了。本质上,池化技术是一种以空间换时间的做法。
线程池就是提前创建好一些线程供我们使用,使用完毕之后并不销毁这些线程,而是放回池中,等待下次继续使用。
而 tomcat 作为 web 界的容器一哥,自然也使用了线程池这种优化手段,为了提高业务处理能力和支持更高的并发,tomcat 还对线程池做了更进一步的优化。

2 原生线程池

讲到 tomcat 优化线程池之前,我们先回顾一下 JAVA 原生的线程池。
JAVA 原生线程池 ThreadPoolExecutor 位于 java.util.concurrent 包下。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
可以看到,ThreadPoolExecutor 提供了 7 个参数。
其工作原理大致如下:
当线程池收到一个新任务,先判断当前线程数是否大于 corePoolSize,如果小于 corePoolSize,就新建一个线程执行任务。
当线程池中的线程数已经达到 corePoolSize,再来新的任务就不会新建线程了,而是将任务投递到 workQueue 中。当核心线程有空闲了,就会从 workQueue 队列 poll 任务去执行。
如果任务非常多,workQueue 已经达到最大任务数量了。这时 maximumPoolSize 就会发挥作用了。线程池会继续创建新线程用于执行任务,直至最大线程数达到 maximumPoolSize。
如果任务继续增加,所有线程都满负荷工作了,队列也满了。此时线程池就会开始执行拒绝策略了,就是我们定义的 handler。
当任务高峰期过去了,临时线程闲置下来了,此时线程池就会从 workQueue 中拉取任务继续执行了。临时线程使用 poll(keepAliveTime, unit)方法拉取任务,如果在指定的 unit 时间内未获取到任务,临时线程就会被销毁回收。

3 tomcat 的线程池

通过对 JAVA 原生线程池的介绍,我们可以知道,当达到最大线程数时,表示线程池已经满负荷运行,无法再接收任务了。此时会执行线程池的拒绝策略,比如抛出异常、丢弃任务等。
那 tomcat 做了什么优化手段来提升并发量呢?
其实原理很简单:当线程数达到最大线程 maximumPoolSize 时,不是立刻执行拒绝策略。而是先尝试将任务投递到任务队列中,如果任务队列此时仍然是满的,再执行拒绝策略。
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
executeInternal(command);
} catch (RejectedExecutionException rx) {
if (getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue) getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
private void executeInternal(Runnable command) {
if (command == null) {
throw new NullPointerException();
}
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) {
return;
}
c = ctl.get();
}
//tomcat在TaskQueue中重写了offer方法
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);
}
}
如上所示,就是 tomcat 线程池的 execute 方法。
executeInternal 方法的代码和 JAVA 原生线程池的 execute 方法是一样的,tomcat 只是将原生执行方法包装了一层。
tomcat 在外层 catch 了 RejectedExecutionException 异常,当异常抛出时,表示任务已满需要执行拒绝策略了。此时 tomcat 尝试将任务再次投递到任务队列,如果投递失败,再抛出一次 RejectedExecutionException 异常,转而去执行拒绝策略。
看到这,有的朋友可能会问了:任务都满了,再投递一次任务到队列中有什么用呢?

3.1 再次投递作用

比如一个平稳运行的系统忽然遇到大量的流量涌入,但是这些请求可能大部分都是一些简单的 CPU 密集型任务,比如简单的计算、查询,并非耗时较长的 IO 任务。
一开始,tomcat 默认的 200 个线程和 10000 的任务队列可能瞬间就被打满了,下一个任务进来时,由于线程池已经满负荷运行,可能就需要执行拒绝策略。
但是这些简单的请求耗时是非常短的,可能几毫秒就已经任务完成。所以过了几毫秒后,任务队列已经有了可继续投递的剩余空间。
那么简单的再次投递任务队列的尝试,可能就会使本该抛异常的请求继续执行。那么反应到应用层面,就是服务器能继续正常处理请求,从而提升了服务器的并发处理能力。

3.2 重写 offer 方法

另外,大家可以在 execute 方法中看到第一行的:submittedCount.incrementAndGet();
默认情况下,tomcat 的任务队列 TaskQueue 的 capacity 是 Integer.MAX_VALUE。
这样的话,当线程数达到核心线程数以后,再来新的任务都会被投递到任务队列中,就没有办法再创建新线程了,这样肯定是不行的。
所以,tomcat 重写了 JDK 的 LinkedBlockingQueue 的 offer 方法。
@Override
public boolean offer(Runnable o) {
// 首先检查parent(指向tomcat线程池)是否为空。
// 如果是空的,则直接调用父类的offer方法,尝试将任务放入队列。
if (parent==null) {
return super.offer(o);
}
// 这里检查线程池中的线程数是否已经达到了最大线程数。
// 如果是,这意味着没有更多的线程可以被创建来执行任务,因此将任务放入队列是合理的。
if (parent.getPoolSizeNoLock() == parent.getMaximumPoolSize()) {
return super.offer(o);
}
// 这一行检查已提交的任务数是否小于或等于当前线程池中的线程数。
// 理论上,如果已提交的任务数没有超过线程池中的线程数,
// 这意味着存在某些线程是空闲的,所以可以把任务放入队列进而让这些空闲线程执行。
if (parent.getSubmittedCount() <= parent.getPoolSizeNoLock()) {
return super.offer(o);
}
// 如果能执行到这一步,则说明提交任务数大于工作线程数量。
// 如果还没到最大线程数量,则返回false,后续在executeInternal创建非核心线程。
if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) {
return false;
}
// 如果能执行到这一步,则说明提交任务数大于工作线程数量并且已经达到最大线程数量。
// 如果能放入任务队列返回true,那么这个任务会等待执行;
// 如果不能放入任务队列返回false,那么在executeInternal会执行拒绝策略。
return super.offer(o);
}

3.3 记录提交的任务个数

public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
....
private final AtomicInteger submittedCount = new AtomicInteger(0);
private final AtomicLong lastContextStoppedTime = new AtomicLong(0L);
....
@Override //覆写JDK线程池回调接口
protected void afterExecute(Runnable r, Throwable t) {
submittedCount.decrementAndGet();
if (t == null) {
stopCurrentThreadIfNeeded();
}
}
public int getSubmittedCount() {
return submittedCount.get();
}
@Override
public void execute(Runnable command) {
execute(command,0,TimeUnit.MILLISECONDS);
}
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
super.execute(command);
} catch (RejectedExecutionException rx) {
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
......
}
总结一下就是,对于一个新任务,有空闲的线程就先用空闲的线程,线程不够用又没达到上限就去创建新线程,线程达到上限就扔到队列排队去,队列满了执行重试机制,重试失败那就没办法了,执行拒绝策略吧。

4 推荐配置

tomcat 的配置参数比较多,但想要达到优化效果,我们并不需要全部关注。下面我们详细介绍一些主要的配置参数,保证让这只老猫跑地更快!

4.1 maxThreads

tomcat 接收客户端请求的最大线程数,也就是同时处理任务的个数,它的默认大小为 200;一般来说,在高并发的 I/O 密集型应用中,这个值设置为 1000 左右比较合理。
一般的服务器操作都包括量方面:计算 (主要消耗 cpu),等待 (io、数据库等)。
第一种极端情况,如果我们的操作是纯粹的计算,那么系统响应时间的主要限制就是 cpu 的运算能力,此时 maxThreads 应该尽量设的小,降低同一时间内争抢 cpu 的线程个数,可以提高计算效率,提高系统的整体处理能力。
第二种极端情况,如果我们的操作纯粹是 IO 操作,例如访问数据库或调用远程接口,那么响应时间的主要限制就变为等待外部资源,此时 maxThreads 应该尽量设的大,这样才能提高同时处理请求的个数,从而提高系统整体的处理能力。此情况下因为 tomcat 同时处理的请求量会比较大,所以需要关注一下 tomcat 的虚拟机内存设置和 linux 的 open file 限制。
以前一直简单的认为多线程等于高效率。其实多线程本身并不能提高 cpu 效率,线程过多反而会降低 cpu 效率。
当 cpu 核心数<线程数时,cpu 就需要在多个线程直接来回切换,以保证每个线程都会获得 cpu 时间,即通常我们说的并发执行。
所以 maxThreads 的配置绝对不是越大越好。
现实应用中,我们的操作都会包含以上两种类型 (计算、等待),所以 maxThreads 的配置并没有一个最优值,一定要根据具体情况来配置。
最好的做法是:在不断测试的基础上,不断调整、优化,才能得到最合理的配置。

4.2 maxConnections 

这个参数是指在同一时间,tomcat 在任何给时间接受并处理的的最大连接数。对于 Java 的阻塞式 BIO,默认值是 maxthreads 的值;对于 Java 的 NIO 模式,maxConnections 默认值是 10000,所以这个参数我们一般保持不动即可。
当达到这个数字时,服务器将接受但不处理一个连接。这个额外的连接将被阻塞,直到正在处理的连接数量低于 maxConnections,此时服务器将开始接受并再次处理新的连接。

4.3 acceptCount 

它表示当线程池中线程都在处理请求时,保存传入连接请求的最大队列长度。队列满时收到的任何请求都将被拒绝。缺省值是 100。我一般会设置成和 maxThreads 设置成一样大的。
简单说明一下上面三个参数的关系:
系统能够保持的连接数:maxConnections+acceptCount,区别是 maxConnections 中的连接可以被调度处理;acceptCount 中的连接只能等待排队。

4.4 线程池其他配置

namePrefix:每个新创建线程的名称前缀
minSpareThreads: 一直处于活跃状态的线程数
maxIdleTime:线程的空闲时间,在超过空闲时间时这些线程则会被销毁
threadPriority:线程池中线程的优先级,默认为 5

4.5 疑问

tomcat 线程池使用自己重写的 TaskQueue 任务队列,并且如果我们使用 tomcat 默认参数配置,那么 TaskQueue 的最大容量是 Integer.MAX_VALUE,如果突发请求比较多并且持续时间比较长,那么 tomcat 会不会耗尽内存而崩溃呢?
其实是不会的,因为 tomcat 在接收链接时做了限流处理。当连接达到 maxConnections 时,请求不会被 socket 接受,而是进入 TCP 的连接队列中。
于是 tomcat 处理请求的过程便是:Acceptor 接收一个请求,若现有线程数量小于 maxThreads 且没有空闲线程,则创建一个新线程处理请求任务,若超过 maxThreads,则放入 TCP 完全连接队列中 (注意,不是线程池中的队列),当队列大于 acceptCount 值时,则报“connection refused”错误。

5 总结

JDK 实现线程池功能比较完善,但是比较适合运行 CPU 密集型任务,不适合 IO 密集型的任务,对于 IO 密集型任务可以间接通过设置线程池参数方式做到。
tomcat 线程池主要是处理 web 请求 (IO 密集型),为了尽可能的处理更多的请求,所以就尽可能不让线程进行入队,提交任务时如果有空闲的线程就先用空闲的线程,线程不够用又没达到最大个数上限就去创建新线程,线程达到最大个数上限就放到队列排队去,如果队列满了执行重试机制放入队列,重试失败只能执行拒绝策略了。
 
 
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 线程池源码解读与实践》
立即购买
登录 后留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。
收起评论
大纲
固定大纲
1 池化技术
2 原生线程池
3 tomcat 的线程池
3.1 再次投递作用
3.2 重写 offer 方法
3.3 记录提交的任务个数
4 推荐配置
4.1 maxThreads
4.2 maxConnections 
4.3 acceptCount 
4.4 线程池其他配置
4.5 疑问
5 总结
显示
设置
留言
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部