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

news2024/9/23 2:36:59

面对海量网络请求,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-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1944634.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

80. 删除有序数组中的重复项 II【 力扣(LeetCode) 】

一、题目描述 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成…

谈一谈爬虫开发工程师

爬虫就只是抓数据的吗&#xff1f;并不是&#xff0c;爬虫工程师的工作不再仅仅是抓取数据&#xff0c;还需要处理其他各种复杂问题&#xff0c;今天我们就来聊聊爬虫开发工程师。 一、 爬虫开发工程师工作内容 爬虫开发工程师是负责编写和维护网络爬虫程序的专业人员。他们的…

计算机技术基础 (bat 批处理)Note4

计算机技术基础 &#xff08;bat 批处理&#xff09;Note4 本节主要讲解一些 bat 批处理文件中的一些特殊符号&#xff0c;包括 , %, > 和 >>, |, ^, & 和 && 和 ||, " ", ,, ;, ()。 回显屏蔽符 回显屏蔽符 : 这个字符在批处理中的意思是关…

Redis一致性与分布式锁

Redis一致性 何为redis一致性 即在项目中&#xff0c;redis缓存中的数据要与数据库当中的数据保持一致。 那么这里&#xff0c;就会有小伙伴要问了&#xff0c;redis缓存中的数据不就是从数据库当中查询出来的吗&#xff1f;怎么会不一致呢&#xff1f; 笔者在这里解答一下…

数字信号||离散系统的冲激响应和阶跃响应(3)

实验三 离散系统的冲激响应和阶跃响应 一、实验目的 (1)加深对离散线性移不变(LSI)系统基本理论的理解&#xff0c;明确差分方程与系统函数之间的关系。 (2)初步了解用MATLAB语言进行离散时间系统研究的基本方法。 (3)掌握求解离散时间系统冲激响应和阶跃响应程序的编写方…

Proxmox8基于PC物理机/服务器安装,初始化,挂载磁盘,安装虚拟机

目录 安装文件 开始安装Proxmox 选择启动菜单&#xff0c;F11 后进入启动菜单选择 按需选择是否关闭RAID 选择对应的U盘 进入安装界面 进入安装启动过程 选择系统盘 设置相关信息 设置IP和开启root远程登录 设置dns 设置网卡ip 设置 ssh 远程登录 开机合并local-l…

Java---String类

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 前言 在C语言中已经涉及到字符串了…

四、GD32 MCU 常见外设介绍 (6) ADC 模块介绍

6.1.ADC 基础知识 12 位逐次逼近式模数转换器模块&#xff08;ADC&#xff09;&#xff0c;可以采样来自于外部输入通道、内部输入通道的模拟信号&#xff0c;采样转换后&#xff0c;转换结果可以按照最低有效位对齐或最高有效位对齐的方式保存在相应的数据寄存器中。 6.2.GD…

go语言Gin框架的学习路线(十)

目录 GORM的CRUD教程 查询 普通查询 定义 User 结构体 查询所有用户 查询第一个用户 总结 条件查询 内联条件 额外查询选项 高级查询 链式操作 Scopes 多个立即执行方法 GORM的CRUD教程 CRUD 是 "Create, Read, Update, Delete"&#xff08;创建、查询…

数字图像处理中的常用特殊矩阵及MATLAB应用

一、前言 Matlab的名称来源于“矩阵实验室&#xff08;Matrix Laboratory&#xff09;”&#xff0c;其对矩阵的操作具有先天性的优势&#xff08;特别是相对于C语言的数组来说&#xff09;。在数字图像处理中&#xff0c;为了提高编程效率&#xff0c;我们可以使用多种方式来创…

【UIE模型-傻瓜式教程】飞桨AI Studio中fork实体抽取任务(打车、快递单)并运行教程

文章目录 fork项目环境与数据准备微调训练验证与测试 fork项目 环境与数据准备 安装paddlenlp&#xff08;尽量装paddlenlp2.4.2&#xff0c;否则会报错&#xff01;&#xff09; 下载打车数据 转换数据格式&#xff0c;并划分训练集、验证集和测试集 微调训练 微调训练&#x…

WiFi通信——STM32通过ESP8266-01S与阿里云通信

嵌入式设计中常用的无线通信方式主要由蓝牙、WiFi、Zigbee、Lora、NB-IOT等等。这些是最常用的&#xff0c;也是在实际项目开发中根据项目的数据通信特点来选择相应的无线通信方式。本设计主要是讲解WiFi在嵌入式开发中的使用。 1. ESP8266-01S烧录固件 WiFi通信的频段和蓝牙一…

论文中的流程图参考图片

写论文的时候&#xff0c;在绘制流程图时&#xff0c;一直纠结n是大写还是小写&#xff0c;用不用斜体&#xff0c;号两边要不要空格。今天找到了一张标准的流程图来参考。图片来自 Zhi-Chang Ba et al, Combination of DCE-MRI and NME-DWI via Deep Neural Network for Predi…

学成在线开心学习

环境配置 第一章 项目介绍&环境搭建 项目背景 项目业务框架 项目技术架构 第二章 内容管理模块 本项目使用mybatis-plus的generator工程生成PO类、Mapper接口、Mapper的xml文件 模块工程 模型类的作用 课程查询接口 controller ApiOperation("课程查询接口&qu…

数字化就是要“用数字说话”运营,按“效果付费”经营

随着数字化技术的迅速发展&#xff0c;企业所处的市场环境发生了深刻的变革。在这个数字化转型时期&#xff0c;数据成为了企业决策的关键依据&#xff0c;“用数字说话”已成为企业运营的基本准则。而“效果付费”作为一种基于实际成果的商业模式&#xff0c;正逐渐受到企业经…

【QAC】Dashboard服务端如何配置

【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 解决Dashboard服务端如何配置的问题。 2、 问题场景 客户想使用Dashboard&#xff0c;Dashboard服务端如何配置。 3、软硬件环境 1、软件版本&#xff1a;HelixQAC23.04 2、机器环境&#xff1a;Windows 64bit 3…

Linux_权限3

Linux所对应的文件类型 1.在Win下&#xff0c;有文件类型&#xff0c;通常通过后缀标识 日常用的就是windows系统这里不做举例. 2.Linux的文件类型不通过后缀区分&#xff08;不代表Linux不用后缀) 其中需要注意的是第一个字符表示文件类型的含义 - :普通文件, 文本, 源代码…

AtCoder Beginner Contest 363(A~D题)

A - Piling Up 思路: 我们只需要找到下一阶段的下限。a / 100 是本阶段1 变成下一阶段&#xff0c;再 * 100变成下限&#xff0c;再与原来的相减即可。 代码: #include<bits/stdc.h> using namespace std; #define N 200010 typedef long long ll; typedef unsigned l…

Biomimetics 综述分享:肌电假肢手的交互操作控制综述

近些年假肢灵巧手成为了热点研究方向。此前有综述研究回顾了包括基于表面肌电信号的预测连续上肢运动的方法、基于表面肌电信号的多任务人机交互应用&#xff0c;以及肌电控制中的各种性能指标。近期&#xff0c;期刊Biomimetics&#xff08;JCR Q1&#xff09;发表了“面向肌电…

定时器+外部中断实现NEC红外线协议解码

一、前言 1.1 功能介绍 随着科技的进步和人们生活水平的提高&#xff0c;红外遥控器已经成为了日常生活中不可或缺的电子设备之一&#xff0c;广泛应用于电视、空调、音响等多种家电产品中。 传统的红外遥控器通常只能实现预设的有限功能&#xff0c;无法满足用户对设备更加智…