Tomcat是如何修正JDK原生线程池bug的?

简介: 为提高处理能力和并发度,Web容器一般会把处理请求的任务放到线程池,而JDK的原生线程池先天适合CPU密集型任务,于是Tomcat改造之。

为提高处理能力和并发度,Web容器一般会把处理请求的任务放到线程池,而JDK的原生线程池先天适合CPU密集型任务,于是Tomcat改造之。

Tomcat 线程池原理

其实ThreadPoolExecutor的参数主要有如下关键点:

  • 限制线程个数

image.png

限制队列长度

image.png

而Tomcat对这俩资源都需要限制,否则高并发下CPU、内存都有被耗尽可能。

因此Tomcat的线程池传参:

// 定制的任务队列
taskqueue = new TaskQueue(maxQueueSize);
// 定制的线程工厂
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,
                               daemon,
                               getThreadPriority()
);
// 定制线程池
executor = new ThreadPoolExecutor(getMinSpareThreads(),
                  getMaxThreads(),
                    maxIdleTime, 
                    TimeUnit.MILLISECONDS,
                    taskqueue,
                    tf);

Tomcat对线程数也有限制,设置:

  • 核心线程数(minSpareThreads)
  • 最大线程池数(maxThreads)

Tomcat线程池还有自己的特色任务处理流程,通过重写execute方法实现了自己的特色任务处理逻辑:

  1. 前corePoolSize个任务时,来一个任务就创建一个新线程
  2. 再有任务,就把任务放入任务队列,让所有线程去抢。若队列满,就创建临时线
  3. 总线程数达到maximumPoolSize,则继续尝试把任务放入任务队列
  4. 若缓冲队列也满了,插入失败,执行拒绝策略

和 JDK 线程池的区别就在step3,Tomcat在线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。

具体又是如何实现的呢?

image.png

public void execute(Runnable command, long timeout, TimeUnit unit) {
    submittedCount.incrementAndGet();
    try {
        // 调用JDK原生线程池的execute执行任务
        super.execute(command);
    } catch (RejectedExecutionException rx) {
       // 总线程数达到maximumPoolSize后,JDK原生线程池会执行默认拒绝策略
        if (super.getQueue() instanceof TaskQueue) {
            final TaskQueue queue = (TaskQueue)super.getQueue();
            try {
                // 继续尝试把任务放入任务队列
                if (!queue.force(command, timeout, unit)) {
                    submittedCount.decrementAndGet();
                    // 若缓冲队列还是满了,插入失败,执行拒绝策略。
                    throw new RejectedExecutionException("...");
                }
            } 
        }
    }
}

定制任务队列

Tomcat线程池的execute方法第一行:

submittedCount.incrementAndGet();

任务执行失败,抛异常时,将该计数器减一:

submittedCount.decrementAndGet();

Tomcat线程池使用 submittedCount 变量维护已提交到线程池,但未执行完的任务数量。


为何要维护这样一个变量呢?


Tomcat的任务队列TaskQueue扩展了JDK的LinkedBlockingQueue,Tomcat给了它一个capacity,传给父类LinkedBlockingQueue的构造器。

public class TaskQueue extends LinkedBlockingQueue<Runnable> {
  public TaskQueue(int capacity) {
      super(capacity);
  }
  ...
}

capacity参数通过Tomcat的maxQueueSize参数设置,但maxQueueSize默认值Integer.MAX_VALUE:当前线程数达到核心线程数后,再来任务的话线程池会把任务添加到任务队列,并且总会成功,就永远无机会创建新线程了。


为解决该问题,TaskQueue重写了LinkedBlockingQueue#offer,在合适时机返回false,表示任务添加失败,这时线程池就会创建新线程。


什么叫合适时机?

public class TaskQueue extends LinkedBlockingQueue<Runnable> {
  ...
   @Override
  // 线程池调用任务队列的方法时,当前线程数 > core线程数
  public boolean offer(Runnable o) {
      // 若线程数已达max,则不能创建新线程,只能放入任务队列
      if (parent.getPoolSize() == parent.getMaximumPoolSize()) 
          return super.offer(o);
      // 至此,表明 max线程数 > 当前线程数 > core线程数
      // 说明可创建新线程:
      // 1. 若已提交任务数 < 当前线程数
      //    表明还有空闲线程,无需创建新线程
      if (parent.getSubmittedCount()<=(parent.getPoolSize())) 
          return super.offer(o);
      // 2. 若已提交任务数 > 当前线程数
      //    线程不够用了,返回false去创建新线程
      if (parent.getPoolSize()<parent.getMaximumPoolSize()) 
          return false;
      // 默认情况下总是把任务放入任务队列
      return super.offer(o);
  }
}

所以Tomcat维护 已提交任务数 是为了在任务队列长度无限时,让线程池还能有机会创建新线程。

目录
相关文章
|
2月前
|
Java 关系型数据库 MySQL
在Linux平台上进行JDK、Tomcat、MySQL的安装并部署后端项目
现在,你可以通过访问http://Your_IP:Tomcat_Port/Your_Project访问你的项目了。如果一切顺利,你将看到那绚烂的胜利之光照耀在你的项目之上!
207 41
|
2月前
|
开发框架 Java 关系型数据库
在Linux系统中安装JDK、Tomcat、MySQL以及部署J2EE后端接口
校验时,浏览器输入:http://[your_server_IP]:8080/myapp。如果你看到你的应用的欢迎页面,恭喜你,一切都已就绪。
283 17
|
3月前
|
Oracle Java 关系型数据库
Tomcat和JDK的详细安装、下载和环境配置指南
以上就是JDK和Tomcat的下载、安装和环境配置的详细步骤。希望这个指南能帮助你顺利完成设置。
206 32
|
2月前
|
关系型数据库 MySQL Java
安装和配置JDK、Tomcat、MySQL环境,以及如何在Linux下更改后端端口。
遵循这些步骤,你可以顺利完成JDK、Tomcat、MySQL环境的安装和配置,并在Linux下更改后端端口。祝你顺利!
168 11
|
2月前
|
Java 关系型数据库 MySQL
在Linux操作系统上设置JDK、Tomcat、MySQL以及J2EE后端接口的部署步骤
让我们总结一下,给你的Linux操作系统装备上最强的军队,需要先后装备好JDK的弓箭,布置好Tomcat的阵地,再把MySQL的物资原料准备好,最后部署好J2EE攻城车,那就准备好进军吧,你的Linux军团,无人可挡!
85 18
|
2月前
|
开发框架 关系型数据库 Java
Linux操作系统中JDK、Tomcat、MySQL的完整安装流程以及J2EE后端接口的部署
然后Tomcat会自动将其解压成一个名为ROOT的文件夹。重启Tomcat,让新“植物”适应新环境。访问http://localhost:8080/yourproject看到你的项目页面,说明“植物”种植成功。
103 10
|
3月前
|
Java 关系型数据库 MySQL
JDK、Tomcat、MariaDB数据库和Profile多环境的配置与使用
以上就是JDK、Tomcat、MariaDB数据库和Profile多环境的配置与使用的基本步骤。这些步骤可能会因为你的具体需求和环境而有所不同,但是基本的思路是一样的。希望这些信息能够帮助你更好地理解和使用这些工具。
115 17
|
10月前
|
Java
安装JDK18没有JRE环境的解决办法
安装JDK18没有JRE环境的解决办法
859 61
|
4月前
|
Java Linux 应用服务中间件
在Rocky Linux 9上安装JDK并配置环境变量!
本教程介绍在Rocky Linux 9上安装JDK并配置环境变量的完整步骤。首先更新系统,清理旧版本JDK相关包及残留文件,确保环境干净。接着搜索并安装所需版本的JDK(如OpenJDK 17),验证安装是否成功。然后查找JDK安装路径,配置全局环境变量`JAVA_HOME`和`PATH`,最后验证环境变量设置。按照此流程操作,可顺利完成Java开发环境搭建,支持多版本切换(如JDK 8/11/17)。生产环境请谨慎操作,避免影响现有服务。
583 21
|
4月前
|
Oracle Java 关系型数据库
课时4:JDK的安装与配置
课时4:JDK的安装与配置 摘要: 1. JDK安装:从Oracle官网下载适合操作系统的JDK版本,确保关闭防火墙,选择正确的位数(如64位),并进行一键式安装。 2. JDK配置:将JDK的bin目录路径(如D:\Java\jdk1.8.0_74\bin)添加到系统环境变量PATH中,确保Java开发命令(如javac、java)可用。配置完成后,重启命令行工具验证安装是否成功。 通过以上步骤,确保Java开发环境的正确搭建。
225 0