剖析Tomcat线程池与JDK线程池的区别和联系!

简介: 剖析Tomcat线程池与JDK线程池的区别和联系!

Tomcat的线程池Executor除了实现Lifecycle接口外,基本和JDK的ThreadPoolExecutor一致,以前是直接继承了JDK的ThreadPoolExecutor,并改写部分逻辑,在最新的代码上(Tomcat 10,2021.7.22以后),甚至是直接抄了一份,改写部分逻辑,然后再通过组合的方式使用。

主要区别是线程工厂、任务队列和拒绝策略上,先看看JDK线程池的执行策略,以及在这几个方面有什么缺陷。

JDK的线程池执行流程

首先需要知道线程池的几个基本概念:核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、阻塞任务队列、拒绝策略。JDK的线程池执行流程如下,当有新任务来临时:

文章内容收录到个人网站,方便阅读hardyfish.top/

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

原生线程池在达到核心线程数时,是优先添加队列,这样比较适合CPU密集型任务(认为新建线程不如让任务排队)。

Tomcat面临的问题

但是Tomcat是属于IO密集型任务,在Tomcat看来,原生的线程池主要有两个问题:

1. 阻塞任务队列

JDK可选的几个阻塞任务队列无非是:LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue、LinkedTransferQueue。后两个显然不适合,候选只有前两个:链表和数组。

对于原生LinkedBlockingQueue,无界,那么线程池就不会满足上述第4步的条件,线程会在达到corePoolSize后不再新建,而是一直加入队列。而作为IO密集型的Tomcat,显然是希望此时创建新线程。

对于原生ArrayBlockingQueue,有界,但是线程池在第5步的时候,就会执行拒绝策略。

2. 拒绝策略

当满足第5步的时候,原生线程池就会执行拒绝策略,具体来说是j.u.c.RejectedExecutionHandler接口。JDK默认了四个实现:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
  • 抛出异常(默认)
  • 直接丢弃
  • 丢弃最早的任务
  • 交给调用者执行

Tomcat的线程池

因为Tomcat的任务属于IO密集型,大概率不会长时间占用CPU资源。即期望任务堆积时,优先创建线程来处理,而不是入队,但是又不想任务被丢弃或交给调用者处理(想始终交给线程池处理)。

所以做了如下改造:

  1. 阻塞任务队列继承了LinkedBlockingQueue(无界),但是自身持有ThreadPoolExecutor的引用,又改写了插入方法:
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
    private transient volatile ThreadPoolExecutor parent = null;   
 
    @Override
    public boolean offer(Runnable o) {
      //we can't do any checks
        if (parent==null) {
            return super.offer(o);
        }
        //we are maxed out on threads, simply queue the object
        // 核心线程数=最大线程数,无脑添加
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
            return super.offer(o);
        }
        //we have idle threads, just add it to the queue
        // 有空闲线程(即核心线程有空闲的),则添加,核心线程自己会去取任务执行
        if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
            return super.offer(o);
        }
        //if we have less threads than maximum force creation of a new thread
        // 核心线程数小于最大线程数,这里改写了JDK线程池的第4步逻辑,不是等队列满了再新建线程,而是优先新建线程
        if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
            return false;
        }
        //if we reached here, we need to add it to the queue
        // 其他情况,无脑添加
        return super.offer(o);
    }
}
  1. 当活跃线程数达到最大线程数,即不能再创建新线程时,将执行拒绝策略。这里对应原生JDK的第5步:

如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

但此时队列其实是没满的,只是满足了上述最后一个if (parent.getPoolSize()

Tomcat的处理也是抛出异常,但是在异常处理时,又强制将任务插入队列:

@Override
public void execute(Runnable command) {
    submittedCount.incrementAndGet();
    try {
        executeInternal(command);
    } catch (RejectedExecutionException rx) {
        if (getQueue() instanceof TaskQueue) {
            // If the Executor is close to maximum pool size, concurrent
            // calls to execute() may result (due to Tomcat's use of
            // TaskQueue) in some tasks being rejected rather than queued.
            // If this happens, add them to the queue.
            final TaskQueue queue = (TaskQueue) getQueue();
            if (!queue.force(command)) {
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
            }
        } else {
            submittedCount.decrementAndGet();
            throw rx;
        }
    }
}

所谓强制插入force其实就是直接super.offer而已。

总结下来就是Tomcat的线程池总是优先尝试新建线程,如果达到上限了,再尝试将任务放入阻塞队列。由于是IO密集型任务,执行时间一般都不会太长,所以阻塞队列大概率不会排队太多造成OOM。

此外Tomcat还自定义了线程工厂,这个比较简单,只是在新建线程时,将调用工厂的类加载器传递给线程上下文加载器:

@Override
public Thread newThread(Runnable r) {
    TaskThread t = new TaskThread(group, r, namePrefix + threadNumber.getAndIncrement());
    t.setDaemon(daemon);
    t.setPriority(threadPriority);
 
    // Set the context class loader of newly created threads to be the
    // class loader that loaded this factory. This avoids retaining
    // references to web application class loaders and similar.
     
    t.setContextClassLoader(getClass().getClassLoader());
    return t;
}

测试验证

写个简单的程序测试下:线程池固定核心线程数1,最大线程数5,一次提交10个任务。

JDK线程池

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 构造线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {}
            });
        }
        System.out.println("active: " + executor.getActiveCount());
        System.out.println("queue: " + executor.getQueue().size());
    }
}

输出:

active: 1

queue: 9

在队列可用的前提下,JDK线程池优先让任务排队。

Tomcat线程池

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 阻塞队列使用Tomcat的TaskQueue
        TaskQueue queue = new TaskQueue();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 5, TimeUnit.SECONDS, queue);
        // 需要设置parent引用
        queue.setParent(executor);
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {}
            });
        }
        System.out.println("active: " + executor.getActiveCount());
        System.out.println("queue: " + executor.getQueue().size());
    }
}

输出:

active: 5

queue: 5

在线程数未达到最大线程数时,Tomcat线程池优先创建线程执行任务。

总结

简单来说:原生JDK的线程池是优先将任务添加到阻塞队列,等队列满再尝试创建线程,适合IO密集型任务。Tomcat属于IO密集型,所以总是优先尝试新建线程,线程池满载了,再添加任务到阻塞队列里排队等待。

Dubbo中也有极其相似的处理:EagerThreadPoolExecutor。

github.com/apache/dubb…

不过个人感觉这种处理方式不太好,在catch块中执行逻辑总觉得不太合适,感觉单独写个reject接口实现来处理比较好,类似如下:

private static class ForceAdd implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        BlockingQueue<Runnable> queue = executor.getQueue();
        if (queue instanceof TaskQueue) {
            final TaskQueue q = (TaskQueue) queue;
            if (!q.force(r)) {
                executor.submittedCount.decrementAndGet();
                throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
            }
        } else {
            executor.submittedCount.decrementAndGet();
            throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
        }
    }
}

不过Tomcat和Dubbo都采用在catch块中处理,暂未找到这样做的相关原因描述。


相关文章
|
18天前
|
存储 算法 Java
JvM JDK JRE 三者区别与联系详解
本文深入解析了Java编程中的三个核心概念:JVM(Java虚拟机)、JDK(Java开发工具包)和JRE(Java运行环境)。JVM是执行Java字节码的虚拟计算机,实现“一次编译,到处运行”;JDK包含JRE及开发工具,用于编写和调试Java程序;JRE负责运行已编译的Java程序。文章详细阐述了它们的功能、组成及应用场景,并通过实例说明其在实际开发中的作用,帮助开发者理解三者联系与区别,提升开发效率与问题解决能力。适合Java初学者及进阶开发者学习参考。
127 3
|
5月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
293 60
【Java并发】【线程池】带你从0-1入门线程池
|
3月前
|
Java
线程池是什么?线程池在实际工作中的应用
总的来说,线程池是一种有效的多线程处理方式,它可以提高系统的性能和稳定性。在实际工作中,我们需要根据任务的特性和系统的硬件能力来合理设置线程池的大小,以达到最佳的效果。
91 18
|
6月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
4月前
|
Java Spring
JDK动态代理和CGLIB动态代理的区别
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: ● JDK动态代理只提供接口的代理,不支持类的代理Proxy.newProxyInstance(类加载器, 代理对象实现的所有接口, 代理执行器) ● CGLIB是通过继承的方式做的动态代理 , 如果某个类被标记为final,那么它是无法使用 CGLIB做动态代理的。Enhancer.create(父类的字节码对象, 代理执行器)
|
5月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
8月前
|
Java 编译器 API
深入解析:JDK与JVM的区别及联系
在Java开发和运行环境中,JDK(Java Development Kit)和JVM(Java Virtual Machine)是两个核心概念,它们在Java程序的开发、编译和运行过程中扮演着不同的角色。本文将深入解析JDK与JVM的区别及其内在联系,为Java开发者提供清晰的技术干货。
134 1
|
8月前
|
Java
.如何根据 CPU 核心数设计线程池线程数量
IO 密集型:核心数*2 计算密集型: 核心数+1 为什么加 1?即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费。
298 4
|
8月前
|
小程序 Java 程序员
JDK 和 JRE 有什么区别
JDK(Java Development Kit)是Java开发工具包,包含编译器、调试器等开发工具,用于开发Java程序。JRE(Java Runtime Environment)是Java运行环境,包含Java虚拟机和类库,用于运行Java程序。简言之,JDK用于编写Java程序,JRE用于运行这些程序。
237 2
|
8月前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
358 2