面对海量网络请求,Tomcat线程池如何进行扩展?

简介: 【10月更文挑战第4天】本文详细探讨了Tomcat线程池相较于标准Java实用工具包(JUC)线程池的关键改进。首先,Tomcat线程池在启动时即预先创建全部核心线程,以应对启动初期的高并发请求。其次,通过重写阻塞队列的入队逻辑,Tomcat能够在任务数超过当前线程数但未达最大线程数时,及时创建非核心线程,而非等到队列满才行动。此外,Tomcat还引入了在拒绝策略触发后重新尝试入队的机制,以提高吞吐量。这些优化使得Tomcat线程池更适应IO密集型任务,有效提升了性能。

面对海量网络请求,Tomcat线程池如何进行扩展?

上篇文章:深入浅出Tomcat网络通信的高并发处理机制说到Tomcat中EndPoint如何高效处理网络通信,其中离不开Tomcat线程池的大力支持

本篇文章就来聊聊Tomcat中的线程池与JUC下的线程池到底有何不同?

java.util.concurrent.ThreadPoolExecutor 是JUC下提供的线程池

org.apache.tomcat.util.threads.ThreadPoolExecutor Tomcat中的线程池对其进行扩展

先回顾下JUC线程池执行流程:

image.png

  1. 如果工作线程数量小于核心线程数量,创建核心线程执行任务
  2. 如果工作线程数量大于等于核心线程数量并且线程池还在运行则尝试将任务加入阻塞队列
  3. 如果任务加入阻塞队列失败(说明阻塞队列已满),并且工作线程小于最大线程数,则创建非核心线程执行任务
  4. 如果阻塞队列已满、并且工作线程数量达到最大线程数量则执行拒绝策略

不理解JUC下线程池的同学可以查看:12分钟从Executor自顶向下彻底搞懂线程池

在这个过程中,我们可以发现JUC的线程池更偏向于CPU密集型任务

当任务数量超过核心线程时,会把任务放到队列中排队(防止线程过多,上下文开销过大),只有队列已满才会创建非核心线程一起来执行任务

(JUC线程池也是可以通过调整参数满足IO密集型任务的,比如把核心线程数量调整为CPU核心数量的两倍)

在面对IO密集型任务时,JUC线程池还有能够优化、提升吞吐量的地方,Tomcat正在这些地方进行扩展:

提前创建核心线程

JUC线程池创建时并不会提前创建好所有的核心线程,而是使用“懒加载”,任务到达时不够核心线程数再创建

Tomcat可能在刚启动就收到大量网络请求,因此创建线程池时不能再像JUC中的线程池使用“懒加载”的方式,而是在创建线程池时就提前创建核心线程

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
   
    //...

    //启动所有核心线程
    prestartAllCoreThreads();
}

创建非核心线程的时机

Tomcat线程池使用自定义的阻塞队列TaskQueue,其继承LinkedBlockingQueue,默认情况下是无界队列(integer最大值)

public class TaskQueue extends LinkedBlockingQueue<Runnable>

在JUC线程池执行流程中,必须等到队列已满才会去创建非核心线程

如果队列无界,那么可能队列堆积太多任务,直到发生OOM也不会创建非核心线程

对于面对IO密集型任务的Tomcat来说,这肯定是不能满足需求的

于是,Tomcat重写TaskQueue队列入队的逻辑,改变创建非核心线程的时机

//parent是该队列对应的线程池,parent就是用来判断什么时候创建非核心线程的
private transient volatile ThreadPoolExecutor parent = null;
//获取当前工作线程数量
parent.getPoolSizeNoLock()
//获取提交的任务数量    
parent.getSubmittedCount()
//调用offer 说明工作线程数量大于等于核心线程数
public boolean offer(Runnable o) {
   
    //没有parent直接入队
    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 后续创建非核心线程
    if (parent.getPoolSizeNoLock() < parent.getMaximumPoolSize()) {
   
        return false;
    }

    //默认入队
    return super.offer(o);
}

ThreadPoolExecutor.executeInternal

    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();
        }
        //工作线程数量大于核心线程数量 调用offer
        //工作线程数量小于最大线程数量 并且 提交任务数量大于工作线程数量 offer返回false
        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);
            }
        }
        //调用addWorker创建非核心线程
        else if (!addWorker(command, false)) {
   
            reject(command);
        }
    }

从源码中可以看到:当线程池的工作线程数量大于核心线程数量、小于最大线程数量并且提交任务数量大于工作线程数量时会去创建非核心线程

也就是说,只要线程数量不超过最大线程,并且任务数量超过当前线程数量,就会去创建非核心线程

这样任务数量过多就去创建非核心线程执行更适合IO密集型的任务

拒绝后再次尝试放入队列

在JUC线程池中,当队列已满并且线程数量达到最大线程数量时会执行拒绝策略

Tomcat线程池捕获拒绝策略的异常后再次尝试把任务放入队列,进而提升吞吐量(拒绝这段时间也可能消费任务,进一步利用队列的容量)

public void execute(Runnable command) {
   
    //原子类自增记录提交任务数量
    submittedCount.incrementAndGet();
    try {
   
        //线程核心流程
        executeInternal(command);
    } catch (RejectedExecutionException rx) {
   
        //捕获拒绝异常
        if (getQueue() instanceof TaskQueue) {
   
            final TaskQueue queue = (TaskQueue) getQueue();
            //拒绝后再次尝试把任务加入队列
            if (!queue.force(command)) {
   
                submittedCount.decrementAndGet();
                throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
            }
        } else {
   
            //其他队列则继续抛出拒绝异常
            submittedCount.decrementAndGet();
            throw rx;
        }
    }
}

运行流程

总结一下,Tomcat线程池为了执行IO密集型任务,与JUC线程池主要的不同在于:提前创建核心线程、任务数量过大时创建非核心线程(即使队列未满),拒绝后再次尝试放入队列

Tomcat线程池运行流程如下:

  1. 核心线程提前创建
  2. 任务数量小于核心线程数量,创建核心线程执行任务(不会执行这步骤,因为核心线程被提前创建)
  3. 任务数量大于核心线程线程,线程池还在运行则尝试将任务加入阻塞队列
  4. 如果任务加入阻塞队列失败(说明阻塞队列已满或任务数量超过当前线程数量),并且线程数量小于最大线程数,则创建非核心线程执行任务
  5. 阻塞队列已满、并且工作线程数量达到最大线程数量则执行拒绝策略
  6. 拒绝后捕获异常再次尝试放到队列中,失败则真正拒绝

默认情况下使用无界队列,只有队列满了才拒绝,当请求速度超过消费速度,堆积任务过多时容易OOM

总结

Tomcat面对IO密集型任务,对JUC线程池进行扩展

为了避免启动时高并发请求访问,将创建核心线程的“懒加载”调整为提前创建

为了防止队列已满才去创建非核心线程,扩展阻塞队列入队逻辑,当任务数量超过线程数量并且不到最大线程数时就去创建非核心线程

为了进一步提升吞吐量,在触发拒绝策略后捕获拒绝异常再次尝试放入队列中

🌠最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Tomcat全解析:架构设计与核心组件实现,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

相关文章
|
2天前
|
机器学习/深度学习 算法 PyTorch
基于图神经网络的大语言模型检索增强生成框架研究:面向知识图谱推理的优化与扩展
本文探讨了图神经网络(GNN)与大型语言模型(LLM)结合在知识图谱问答中的应用。研究首先基于G-Retriever构建了探索性模型,然后深入分析了GNN-RAG架构,通过敏感性研究和架构改进,显著提升了模型的推理能力和答案质量。实验结果表明,改进后的模型在多个评估指标上取得了显著提升,特别是在精确率和召回率方面。最后,文章提出了反思机制和教师网络的概念,进一步增强了模型的推理能力。
16 4
基于图神经网络的大语言模型检索增强生成框架研究:面向知识图谱推理的优化与扩展
|
1月前
|
缓存 负载均衡 网络协议
|
2月前
|
Dubbo Java 应用服务中间件
剖析Tomcat线程池与JDK线程池的区别和联系!
剖析Tomcat线程池与JDK线程池的区别和联系!
140 0
剖析Tomcat线程池与JDK线程池的区别和联系!
|
2月前
|
Java Linux
【网络】高并发场景处理:线程池和IO多路复用
【网络】高并发场景处理:线程池和IO多路复用
55 2
|
3月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
3月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。
|
4月前
|
API Windows
揭秘网络通信的魔法:Win32多线程技术如何让服务器化身超级英雄,同时与成千上万客户端对话!
【8月更文挑战第16天】在网络编程中,客户/服务器模型让客户端向服务器发送请求并接收响应。Win32 API支持在Windows上构建此类应用。首先要初始化网络环境并通过`socket`函数创建套接字。服务器需绑定地址和端口,使用`bind`和`listen`函数准备接收连接。对每个客户端调用`accept`函数并在新线程中处理。客户端则通过`connect`建立连接,双方可通过`send`和`recv`交换数据。多线程提升服务器处理能力,确保高效响应。
59 6
|
4月前
|
缓存 网络协议 Linux
扩展Linux网络栈
扩展Linux网络栈
83 3
|
4月前
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享安卓与iOS开发中的线程管理比较
【8月更文挑战第30天】本文将探讨网络安全与信息安全的重要性,并分享关于网络安全漏洞、加密技术和安全意识的知识。我们将了解常见的网络攻击类型和防御策略,以及如何通过加密技术和提高安全意识来保护个人和组织的信息安全。