Tomcat线程池原理(上篇:初始化原理)

2024-03-01 1486阅读

温馨提示:这篇文章已超过389天没有更新,请注意相关的内容是否还可用!

文章目录

  • 前言
  • 正文
    • 一、从启动脚本开始分析
    • 二、ProtocolHandler 的启动原理
    • 三、AbstractEndPoint 的启动原理
    • 四、创建默认线程池
    • 五、参数配置原理
      • 5.1 常规的参数配置
      • 5.2 自定义线程池
      • 5.3 测试自定义线程

        前言

        在Java Web的开发过程中,Tomcat常用的web容器。SpringBoot之前,我们用的是单独的 Tomcat,SpringBoot时代,嵌入了Tomcat。

        在Jdk中,JUC内有线程框架,以及可以自定义参数配置的 TreadPoolExecutor。Tomcat内也实现了自己的线程池。

        所谓线程池,是被用来处理传入的 HTTP 请求的。

        当客户端发送请求时,Tomcat 会从线程池中获取一个可用的线程来处理该请求。处理完请求后,线程将返回线程池,并在下一个请求到来时再次被重用。

        究其原因,是JUC内的线程池不符合Tomcat的使用场景。

        • Jdk中的线程池,是cpu密集型(也就是偏计算,处理完了可以去队列再取任务)
        • Tomcat的应用场景,却大多是IO密集型的。(也就是要求IO尽量不要阻塞,任务先处理,实在处理不了了,再进阻塞队列)

          下图是JUC中线程池处理任务的流程:

          Tomcat线程池原理(上篇:初始化原理)

          与JUC中明显不同的一点是,Tomcat为了处理IO,减少阻塞的情况,

          本系列文章就是专门探讨Tomcat中线程池的原理,分为上下两篇,本文是上篇,主要介绍Tomcat中线程池的初始化原理。

          本系列文章基于SpringBoot2.7.6,其内嵌的tomcat版本是9.0.69。

          同系列文章:Tomcat线程池原理(下篇:工作原理)

          正文

          本系列文章核心内容是Tomcat的线程池原理,因此在画图,文字描述时会忽略部分不涉及的内容。

          一、从启动脚本开始分析

          使用过Tomcat的同学都知道,我们单独的启动tomcat时,是从脚本入手的。

          启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh)

          在startup.bat 脚本中, 调用了catalina.bat。

          在catalina.bat 脚本文件中,调用了BootStrap 中的main方法。

          后续的操作如下图:

          Tomcat线程池原理(上篇:初始化原理)

          简而言之,就是逐级的 init() 和 start()。

          而本文的关注点,就是 ProtocolHandler 的 start(),也就是图中的最后一步。

          二、ProtocolHandler 的启动原理

          Tomcat线程池原理(上篇:初始化原理)

          关键在于 EndPoint的 start()。

          而在Tomcat 中,会执行到 AbstractEndPoint的 start()。具体代码如下:

          public final void start() throws Exception {
              if (bindState == BindState.UNBOUND) {
                  bindWithCleanup();
                  bindState = BindState.BOUND_ON_START;
              }
              startInternal();
          }
          public abstract void startInternal() throws Exception;
          

          也就是说真正的启动方法是AbstractEndPoint 子类实现的startInternal()。

          三、AbstractEndPoint 的启动原理

          在Tomcat中,有3个AbstractEndPoint的子类。

          在8.5/9.0版本中,使用的是其中的 NioEndPoint类。

          本文就使用默认的 NioEndPoint 进行分析。

          接第二小节, NioEndPoint 在执行startInternal()时,会判断是否存在线程池,如果没有,会创建默认的线程池。对应代码如下:

          @Override
          public void startInternal() throws Exception {
              if (!running) {
                  running = true;
                  paused = false;
                  if (socketProperties.getProcessorCache() != 0) {
                      processorCache = new SynchronizedStack(SynchronizedStack.DEFAULT_SIZE,
                              socketProperties.getProcessorCache());
                  }
                  if (socketProperties.getEventCache() != 0) {
                      eventCache = new SynchronizedStack(SynchronizedStack.DEFAULT_SIZE,
                              socketProperties.getEventCache());
                  }
                  if (socketProperties.getBufferPool() != 0) {
                      nioChannels = new SynchronizedStack(SynchronizedStack.DEFAULT_SIZE,
                              socketProperties.getBufferPool());
                  }
                  // 如果没自定义线程池,则创建默认工作线程池
                  if (getExecutor() == null) {
                      createExecutor();
                  }
                  initializeConnectionLatch();
                  // Start poller thread
                  poller = new Poller();
                  Thread pollerThread = new Thread(poller, getName() + "-Poller");
                  pollerThread.setPriority(threadPriority);
                  pollerThread.setDaemon(true);
                  pollerThread.start();
                  startAcceptorThread();
              }
          }
          

          四、创建默认线程池

          根据第三小节的分析,在没自定义线程池,或者配置线程池时,会自动创建一个线程池。代码如下:

              public void createExecutor() {
                  internalExecutor = true;
                  TaskQueue taskqueue = new TaskQueue();
                  TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
                  executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
                  taskqueue.setParent( (ThreadPoolExecutor) executor);
              }
          

          注意,ThreadPoolExecutor 不是JUC中的线程池了,其是Tomcat自己实现的线程池。

          五、参数配置原理

          日常工作中,总会遇到需要自己制定Tomcat线程池参数的情况。这一小节就来说明一下。

          在Tomcat中,TomcatWebServerFactoryCustomizer 负责配置自定义参数。

          在自动配置类 EmbeddedWebServerFactoryCustomizerAutoConfiguration 中配置了如下内容:

          @Configuration(proxyBeanMethods = false)
          @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
          public static class TomcatWebServerFactoryCustomizerConfiguration {
          	@Bean
          	public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
          		ServerProperties serverProperties) {
          			return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
          	}
          }
          

          5.1 常规的参数配置

          普通的参数配置可以参考ServerProperties 中的内容。

          # Tomcat连接数相关参数
          # 最大连接数,默认8192,一般要大于(tomcat.threads.max + tomcat.accept-count)
          server.tomcat.max-connections=300
          # 当所有工作线程都被占用时,新的连接将会放入等待队列中的最大容量,默认100
          server.tomcat.accept-count=50
          # Tomcat线程池相关参数
          # 最大线程池大小,默认200
          server.tomcat.threads.max=200
          # 最小工作空闲线程数(核心线程数),默认10
          server.tomcat.threads.min-spare=12
          

          5.2 自定义线程池

          如果普通的参数配置,不能满足你的需求,则需要自定义线程池。

          定义自己的类,继承 TomcatWebServerFactoryCustomizer ,然后重写customize即可。

          核心思路是,在AbstractProtocol 中设置线程池。

          以下是我的示例:

          package org.feng.demos.web;
          import org.apache.coyote.AbstractProtocol;
          import org.apache.coyote.ProtocolHandler;
          import org.apache.tomcat.util.threads.TaskQueue;
          import org.apache.tomcat.util.threads.TaskThreadFactory;
          import org.apache.tomcat.util.threads.ThreadPoolExecutor;
          import org.springframework.boot.autoconfigure.web.ServerProperties;
          import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer;
          import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory;
          import org.springframework.core.env.Environment;
          import org.springframework.stereotype.Component;
          import java.util.concurrent.TimeUnit;
          /**
           * 自定义tomcat线程池
           *
           * @author feng
           */
          @Component
          public class MyTomcatWebServerFactoryCustomizer extends TomcatWebServerFactoryCustomizer {
              public MyTomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
                  super(environment, serverProperties);
              }
              @Override
              public void customize(ConfigurableTomcatWebServerFactory factory) {
                  super.customize(factory);
                  // 自定义tomcat线程池
                  System.out.println("自定义tomcat线程池--start");
                  // 自定义tomcat线程池
                  factory.addConnectorCustomizers((connector) -> {
                      ProtocolHandler handler = connector.getProtocolHandler();
                      if (handler instanceof AbstractProtocol) {
                          AbstractProtocol protocol = (AbstractProtocol) handler;
                          TaskQueue taskqueue = new TaskQueue();
                          TaskThreadFactory tf = new TaskThreadFactory("feng" + "-exec-", true, 5);
                          ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, taskqueue, tf);
                          protocol.setExecutor(threadPoolExecutor);
                          taskqueue.setParent(threadPoolExecutor);
                      }
                  });
                  System.out.println("自定义tomcat线程池--end");
              }
          }
          

          5.3 测试自定义线程

          定义如下方法:

          // http://127.0.0.1:8080/hello?name=lisi
          @RequestMapping("/hello")
          @ResponseBody
          public String hello(@RequestParam(name = "name", defaultValue = "unknown user") String name) {
              System.out.println("当前线程名:" + Thread.currentThread().getName());
              return "Hello " + name;
          }
          

          调用时,控制台打印:

          Tomcat线程池原理(上篇:初始化原理)

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]