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


    前言

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

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

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

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

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

    下图是JUC中线程池处理任务的流程:
    在这里插入图片描述

    与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方法。
    后续的操作如下图:
    在这里插入图片描述
    简而言之,就是逐级的 init()start()
    而本文的关注点,就是 ProtocolHandlerstart(),也就是图中的最后一步。

    二、ProtocolHandler 的启动原理

    在这里插入图片描述
    关键在于 EndPointstart()

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

    public final void start() throws Exception {
        if (bindState == BindState.UNBOUND) {
            bindWithCleanup();
            bindState = BindState.BOUND_ON_START;
        }
        startInternal();
    }
    
    
    public abstract void startInternal() throws Exception;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    也就是说真正的启动方法是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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    四、创建默认线程池

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

        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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意,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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    调用时,控制台打印:
    在这里插入图片描述

  • 相关阅读:
    idea 打 jar 包以及运行使用
    Paxos分布式共识算法
    构造函数_Map构造函数
    Java中SFTP的使用
    从零使用GAN(生成对抗网络)进行图像生成
    23111709[含文档+PPT+源码等]计算机毕业设计基于Spring Boot智能无人仓库管理-进销存储
    指针传2(续集)
    Spring Boot集成第三方登录之微博登录
    项目环境配置
    产品经理-研发流程-敏捷开发-迭代-需求评审及产品规划(15)
  • 原文地址:https://blog.csdn.net/FBB360JAVA/article/details/136197034