文章目录
- 文章导图
- 前言
- 核心概念解释
- 异步任务执行的线程池(Spring Boot默认线程池)
- 内嵌Tomcat的HTTP请求处理线程池
- 区别汇总
- SpringBoot 内置Tomcat线程池-处理Http请求
- Tomcat参数配置
- 参数默认配置
- Connector运行模式
- 如何查看参数配置
- 对应的启动流程
- 请求流程
- accept-count和max-connections这两者有什么区别
- max-connections
- accept-count
- 对比
- 代码实践
- SpringBoot默认线程池-处理异步任务
- 默认线程池参数
- 如何查看
- 如何自定义线程池?
- 如何使用?
- 请求流程
- 后续TODO
线程池系列文章可参考下表,目前已更新完毕…
线程池系列: 文章 Java基础线程池 深入剖析Java线程池的核心概念与源码解析:从Executors、Executor、execute逐一揭秘 CompletableFuture线程池 从用法到源码再到应用场景:全方位了解CompletableFuture及其线程池 SpringBoot默认线程池(@Async和ThreadPoolTaskExecutor) 探秘SpringBoot默认线程池:了解其运行原理与工作方式(@Async和ThreadPoolTaskExecutor) SpringBoot默认线程池和内置Tomcat线程池 你是否傻傻分不清SpringBoot默认线程池和内置Tomcat线程池? 文章导图
前言
在Java应用程序中,线程池是一种用于管理和重用线程的机制。线程池可以显著提高多线程应用程序的性能,避免不必要的线程创建和销毁开销,同时有效控制并发线程数量,防止系统资源被耗尽。
对于SpringBoot程序,我们知道它是会有一个内置的Tomcat,但是我自己之前一直对于SpringBoot默认线程池和SpringBoot内置Tomcat线程池概念不是很清晰,很容易混淆,甚至以为它们就是一个东西,经过这次深入了解以后,发现两个完全是不同的东西,接下来跟我一起探索吧~
核心概念解释
在Spring Boot应用中,涉及到两种常见的线程池:Spring Boot内部使用的异步任务执行的线程池(通常指的是通过@Async注解定义的异步任务,或者是直接注入自定义的线程池进行使用),以及内嵌Tomcat容器用于处理HTTP请求的线程池。它们的主要区别在于用途、配置方式和管理策略。
这里用一张图帮助理解,Spring Boot默认线程池主要处理应用程序中的后台任务,而Tomcat线程池专门处理Web层面的HTTP请求。
异步任务执行的线程池(Spring Boot默认线程池)
- 用途:用于处理非HTTP请求的后台任务,如发送邮件、执行数据库长时间操作等。
- 配置:通常通过实现AsyncConfigurer接口或者在配置文件中定义相关参数来配置(例如,spring.task.execution.pool前缀的参数)。
- 管理:由Spring框架管理,可以在应用中自定义扩展,如定义线程池大小、队列容量等。
- 自定义:开发者可以通过实现WebMvcConfigurer配置自定义的TaskExecutor。
内嵌Tomcat的HTTP请求处理线程池
- 用途:专门用于处理入站的HTTP请求。
- 配置:在Spring Boot的配置文件(application.properties或application.yml)中,通过设置server.tomcat相关选项来配置线程池参数(例如,max-threads、accept-count等)。
- 管理:由内嵌的Web容器管理(默认是Tomcat),和Spring框架的@Async任务是分开的。
- 自定义:较少直接控制,主要通过配置文件中设置的属性值来控制。
区别汇总
- 用途区分:Spring Boot默认线程池主要处理应用程序中的后台任务,而Tomcat线程池专门处理Web层面的HTTP请求。
- 配置差异:Tomcat线程池主要通过server.tomcat前缀的配置属性进行配置,而Spring Boot默认线程池的配置更灵活,可以通过实现接口或通过配置文件进行。
- 管理方式:Tomcat线程池是由Tomcat容器直接管理,而Spring Boot中的默认线程池是由Spring框架管理。
- 扩展性:Spring Boot中的默认线程池可以更容易地通过代码进行自定义和扩展,Tomcat线程池的扩展性较低。
SpringBoot 内置Tomcat线程池-处理Http请求
Tomcat参数配置
参数默认配置
server.tomcat.accept-count:等待队列长度,当可分配的线程数全部用完之后,后续的请求将进入等待队列等待,等待队列满后则拒绝处理,默认100。 server.tomcat.max-connections:最大可被连接数,默认10000 server.tomcat.max-threads:最大工作线程数,默认200, server.tomcat.min-spare-threads:最小工作线程数,初始化分配线程数,默认10
-
默认配置下,连接超过10000后会出现拒绝连接情况
-
默认配置下,触发的请求超过200+100后拒绝处理(最大工作线程数+等待队列长度)
Connector运行模式
默认设置中,Tomcat的最大线程数是200,最大连接数是10000。支持的并发量是指连接数,200个线程如何处理10000条连接的?
目前Tomcat有三种处理连接的模式,一种是BIO,一个线程只处理一个连接,另一种就是NIO,一个线程处理多个连接。由于HTTP请求不会太耗时,而且多个连接一般不会同时来消息,所以一个线程处理多个连接没有太大问题。还有一种是apr模式,这里不做深入讨论。
Tomcat启动的时候,可以通过log看到Connector使用的是哪一种运行模式:我们可以发现就是NIO模式
如何查看参数配置
Tomcat创建线程池的时候底层还是利用JDK的ThreadPoolExecutor,所以在这个地方打个断点即可!
对应的启动流程
springboot启动的时候,底层还是依赖于spring的,所以会调用org.springframework.context.support.AbstractApplicationContext#refresh
在这里之后的流程如下,最终会启动org.springframework.boot.web.embedded.tomcat.TomcatWebServer#start,接着就会创建对应的线程池了,这里最终也是用JDK里面的线程池创建的!
请求流程
这些默认的配置我们当然也可以自定义重新配置过,如下所示,我们来看看不同线程情况下,对应的请求流程是怎么样的!
server: port: 9000 tomcat: accept-count: 10 #等待队列长度 max-connections: 100 #一瞬间最大支持的并发的连接数 max-threads: 20 #最大工作线程数量 min-spare-threads: 5 #最小工作线程数量
网上说的配置参数(仅供参考):
线程数的经验值为:1核2G内存,线程数经验值200;4核8G内存, 线程数经验值800。
(4核8G内存单进程调度线程数800-1000,超过这个并发数之后,将会花费巨大的时间在CPU调度上)
根据提供的Tomcat参数配置,我们可以分析在不同线程数下的请求处理流程情况:
- max-threads: 20:这是Tomcat可用于处理请求的最大线程数。当请求进来时,Tomcat会尝试为每个请求分配一个线程来处理。如果同时到达的请求数不超过20个,Tomcat可以同时处理这些请求。
- min-spare-threads: 5:这是Tomcat维护的最小空闲(备用)线程数。Tomcat会确保始终至少有5个线程处于空闲状态,以备突然大量请求到来时使用。
- max-connections: 100:这是Tomcat可以接受的最大连接数。这个配置是针对NIO(非阻塞I/O)模式下的设置,表示服务器接受客户端连接的最大数量。如果连接数达到这个数量,新的连接将无法建立。
- accept-count: 10:这是当所有可用的处理请求的线程都在工作时,Tomcat能够在其连接队列中排队的最大连接数。如果请求太多,处理线程已满,额外的请求会放到一个等待队列里,直到有可用的处理线程。若等待队列也满了,再有新的请求则被拒绝。
下面根据不同的情况给出对应的请求处理流程:
- 请求量小于等于20(max-threads):所有请求都可以立即由一个处理线程处理,无需进入等待队列。
- 请求量超过20但小于等于30(max-threads + accept-count):超过20的请求将会被放入等待队列。若处理线程完成了部分任务并变为空闲,队列中的请求将按照先进先出的顺序被取出处理。
- 请求量超过30(max-threads + accept-count):Tomcat的处理线程已满,且等待队列也达到上限,新的请求无法被排队必须被拒绝。
- 同时活跃连接数超过100(max-connections):即使处理线程和等待队列仍有空间,新的连接也无法建立,因为已经达到了最大连接数。
适当调整这些参数,可以确保Tomcat根据可用资源和预期负载以最优方式处理请求。然而,合理的参数配置需要根据实际运行环境和需求来进行调整,并且在实际部署时进行持续观察和调优。
accept-count和max-connections这两者有什么区别
accept-count 和 max-connections 是Tomcat配置中两个重要但概念上不同的参数。理解这两个参数及其区别对于优化Tomcat性能和容量规划非常关键。
max-connections
- 定义:指定Tomcat可以接受的最大并发连接数。
- 作用域:这个值主要关注于网络层面上的并发连接数,包括正在被处理的连接和等待处理的连接。它是对Tomcat服务端可以打开的最大TCP连接数的限制。
- 例子:假设max-connections设置为100,这意味着Tomcat服务器最多同时支持100个网络连接。无论这些连接是正在被线程处理中的请求,还是处于建立连接状态但尚未分配到处理线程的请求,总数不会超过100。如果尝试建立的连接超过这个数值,那么新的连接会被拒绝。
accept-count
- 定义:指定当所有可以用于接收和处理请求的线程都被使用时,能够放入等待队列中的连接请求的最大数目。
- 作用域:这个值关注于服务器能够“暂时存储”的并未立即处理的连接请求的数量。当所有处理线程都繁忙时,新的连接请求将会进入这个队列等待直到处理线程可用。
- 例子:如果accept-count设置为10,并且max-threads设置为20,这意味着最多有20个请求可以同时被处理,当这20个线程全部繁忙时,额外的10个连接请求可以被放入等待队列。如果有线程处理完它的当前请求变为空闲,则队列中的请求将被移到处理线程中开始处理。如果队列也满了(即同时有30个活跃请求),额外的请求将被拒绝。
对比
从本质上说,max-connections 更多地涉及到服务器对外提供服务的网络连接的能力,而 accept-count 则是处理资源(如线程)饱和时的缓冲能力。前者是决定服务能够接收多少并发连接的网络层面的限制,后者则是应用层面上关于当处理瓶颈出现时如何管理新的入站连接的策略。
将上述例子结合起来考虑,假如同时有100个请求向服务器发起连接,根据max-connections,这些连接都可以成功建立。但是,因为max-threads设置为20,所以只有20个请求能够同时被处理,剩下的请求里,最多有10个能够进入等待队列(按照accept-count定义),而超过这30(20+10)的部分请求则将因为无法被即时处理也无法进入等待队列而被拒绝。
代码实践
一开始我还在寻思着如何模拟上面说的参数配置的流程,后面一想,我们springboot一启动起来不就是一个天然的Tomcat线程池吗,
server: port: 9000 tomcat: accept-count: 10 #等待队列长度 max-connections: 100 #一瞬间最大支持的并发的连接数 max-threads: 20 #最大工作线程数量 min-spare-threads: 5 #最小工作线程数量
上面我们已经配置过了对应参数,编写一个controller用于请求
@RestController public class TestController { @SneakyThrows @RequestMapping("/testTomcatThreadPool") public void testTomcatThreadPool() { System.out.println("线程:"+Thread.currentThread().getName()); //故意设置睡眠10分钟,模拟线程被占用了! ThreadUtil.sleep(10, TimeUnit.MINUTES); } }
接下来我们只要模拟客户端去访问刚刚的controller请求即可,我们简单粗暴一点,直接新开一个线程代表一个客户端的请求。
@SneakyThrows @Test public void test() { int threadCount = 200; CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { int finalI = i; new Thread(() -> { //System.out.println("开始启动线程执行:" + finalI); HttpResponse execute = null; try { execute = HttpUtil.createGet("localhost:9000/testTomcatThreadPool").execute(); } catch (Exception e) { System.out.println("未获得连接异常:"+finalI); e.printStackTrace(); } latch.countDown(); // 完成任务,计数器减一 }).start(); } latch.await(); // 等待所有线程完成任务 System.out.println("所有线程执行完毕,结束程序"); }
我们用最简单的方式验证最多只有20个线程在同时工作,直接ctrl+f查找对应的关键字,发现只有20个!
这里总的线程是200个,而我们设置的最大max-connections是100,所以超过以后的都会拒绝!
SpringBoot默认线程池-处理异步任务
Spring Boot 本身并不提供一个默认的线程池配置,而是依赖于 Spring Framework 中 TaskExecutor 抽象的默认配置。在 Spring Framework 中,TaskExecutor 是用来处理多线程任务的抽象接口,而 ThreadPoolTaskExecutor 是其一个具体的实现,它使用了一个 Java 的 java.util.concurrent.ThreadPoolExecutor 作为其核心。
在springboot当中,根据 官方文档的说明,如果没有配置线程池的话,springboot会自动配置一个ThreadPoolTaskExecutor 线程池到bean当中,我们只需要按照他的方式调用就可以了!!
- ThreadPoolExecutor:这个是JAVA自己实现的线程池执行类,基本上创建线程池都是通过这个类进行的创建!
- ThreadPoolTaskExecutor :这个是springboot基于ThreadPoolExecutor实现的一个线程池执行类。
默认线程池参数
Spring Boot在没有自定义线程池配置的情况下,会自动配置一个ThreadPoolTaskExecutor作为默认线程池。根据官方文档和相关资源,以下是默认的线程池参数:
- 核心线程数 (corePoolSize): 8
- 最大线程数 (maxPoolSize): Integer.MAX_VALUE (无限制)
- 队列容量 (queueCapacity): Integer.MAX_VALUE (无限制)
- 空闲线程保留时间 (keepAliveSeconds): 60秒
- 线程池拒绝策略 (RejectedExecutionHandler): AbortPolicy(默认策略,超出线程池容量和队列容量时抛出RejectedExecutionException异常)
这些参数可以通过在application.properties或application.yml文件中设置来进行自定义调整。例如:
# 核心线程数,默认为8 spring.task.execution.pool.core-size # 最大线程数,默认为Integer.MAX_VALUE spring.task.execution.pool.max-size # 任务等待队列容量,默认为Integer.MAX_VALUE spring.task.execution.pool.queue-capacity # 空闲线程等待时间,默认为60s。如果超过这个时间没有任务调度,则线程会被回收 spring.task.execution.pool.keep-alive # 是否允许回收空闲的线程,默认为true spring.task.execution.pool.allow-core-thread-timeout # 线程名前缀 spring.task.execution.thread-name-prefix=task-
如何查看
在 Spring Boot 中,默认的线程池由 TaskExecutorBuilder 类负责创建,它通常使用 ThreadPoolTaskExecutor 来配置默认的线程池。虽然默认的线程池参数可以根据不同的 Spring Boot 版本或特定配置而有所不同,但通常情况下,Spring Boot 默认的线程池参数如下:
在org.springframework.boot.autoconfigure.task包的TaskExecutionAutoConfiguration.java是SpringBoot默认的任务执行自动配置类。
从@EnableConfigurationProperties(TaskExecutionProperties.class)可以知道开启了属性绑定到TaskExecutionProperties.java的实体类上
打个断点可以看到默认参数如下:
进入到TaskExecutionProperties.java类中,看到属性绑定以spring.task.execution为前缀。默认线程池的核心线程数coreSize=8,最大线程数maxSize = Integer.MAX_VALUE,以及任务等待队列queueCapacity = Integer.MAX_VALUE
因为Integer.MAX_VALUE的值为2147483647(2的31次方-1),所以默认情况下,一般任务队列就可能把内存给堆满了。我们真正使用的时候,还需要对异步任务的执行线程池做一些基础配置,以防止出现内存溢出导致服务不可用的问题。
如何自定义线程池?
我们可以通过在应用的配置文件(如 application.properties 或 application.yml)中自定义这些参数,以满足我们的一些特定需求。
但是如果需要更加精确地控制线程池的参数,您也可以在配置类中自定义一个 ThreadPoolTaskExecutor bean,并根据具体需求设置相应的参数。
以下是一个示例,展示如何在 Spring Boot 应用中配置一个自定义的线程池:
@Configuration public class ThreadPoolConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); //设置线程池参数信息 taskExecutor.setCorePoolSize(10); taskExecutor.setMaxPoolSize(50); taskExecutor.setQueueCapacity(200); taskExecutor.setKeepAliveSeconds(60); taskExecutor.setThreadNamePrefix("myExecutor--"); taskExecutor.setWaitForTasksToCompleteOnShutdown(true); //线程池中任务等待时间,超过等待时间直接销毁 taskExecutor.setAwaitTerminationSeconds(60); //修改拒绝策略为使用当前线程执行 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //初始化线程池 taskExecutor.initialize(); return taskExecutor; } }
如何使用?
这里使用很简单,只需要像普通的spring bean一样注入即可,它会去根据name匹配 @Bean(“taskExecutor”) 这个线程池
@Resource(name="taskExecutor") ThreadPoolTaskExecutor taskExecutor; //会去匹配 @Bean("taskExecutor") 这个线程池
如果是使用的@Async注解,只需要在注解里面指定bean的名称就可以切换到对应的线程池去了。如下所示:
请注意,Spring Boot 提供了自动配置选项,可以根据应用程序的类路径和其他条件自动配置 ThreadPoolTaskExecutor。例如,如果你使用 @EnableAsync 注解并且没有显式配置 TaskExecutor,Spring Boot 可能会为你提供一个默认的 TaskExecutor 实现。但是,这个默认配置的具体参数取决于 Spring Boot 的版本和你的应用程序的依赖。
@Async("taskExecutor") public void hello(String name){ logger.info("异步线程启动 started."+name); }
请求流程
这里的请求流程其实就是我们常见的JDK的线程池流程
假如我们的application.properties配置参数如下,接下来我们来看看,根据不同的线程数对应的请求流程是怎么样的:
# 核心线程数,默认为8 spring.task.execution.pool.core-size=3 # 最大线程数,默认为Integer.MAX_VALUE spring.task.execution.pool.max-size=20 # 任务等待队列容量,默认为Integer.MAX_VALUE spring.task.execution.pool.queue-capacity=10 # 空闲线程等待时间,默认为60s。如果超过这个时间没有任务调度,则线程会被回收 spring.task.execution.pool.keep-alive=60 # 是否允许回收空闲的线程,默认为true spring.task.execution.pool.allow-core-thread-timeout=true # 线程名前缀 spring.task.execution.thread-name-prefix=task-
下面是根据这些配置参数,不同的线程数时对应的请求流程:
- 核心线程数(core-size)= 3:
- 当应用程序启动时,线程池将创建3个线程作为核心线程。
- 这些核心线程将一直存在,除非设置了 allow-core-thread-timeout=true,在这种情况下,如果线程空闲时间超过 keep-alive 时间(60秒),它们也会被回收。
- 最大线程数(max-size)= 20:
- 当所有核心线程都在忙于处理任务时,新来的任务将首先进入等待队列(queue-capacity)。
- 如果等待队列已满(即队列中的任务数达到了 queue-capacity 的10个任务),线程池将开始创建新的线程,直到线程数达到 max-size(20个线程)。
- 任务等待队列容量(queue-capacity)= 10:
- 当核心线程都在忙时,新任务会被放入等待队列中。
- 如果队列已满,新的任务将无法被接受,除非线程池开始创建新的线程。
- 空闲线程等待时间(keep-alive)= 60s:
- 对于非核心线程(即线程数大于 core-size 的线程),如果它们空闲时间超过60秒,它们将被回收。
- 如果 allow-core-thread-timeout=true,核心线程也会在空闲60秒后被回收。
- 是否允许回收空闲的线程(allow-core-thread-timeout)= true:
- 这意味着核心线程也可以被回收,如果它们空闲时间超过了 keep-alive 时间。
- 线程名前缀(thread-name-prefix)= task-:
- 所有线程的名称将以 “task-” 作为前缀,这样可以方便地在日志中识别出哪些线程属于这个线程池。
请求流程概述:
- 当一个新任务到达时,首先检查是否有空闲的核心线程可用。
- 如果有,一个核心线程将立即处理这个任务。
- 如果没有空闲的核心线程,任务将被放入等待队列。
- 如果队列已满,线程池将创建一个新的线程(如果当前线程数小于 max-size)。
- 如果线程数已经达到 max-size,新的任务将无法被接受,并且可能会被拒绝(根据拒绝策略)。
- 非核心线程在空闲时间超过 keep-alive 时间后将被回收,但核心线程只有在 allow-core-thread-timeout=true 时才会被回收。
需要注意的是,如果线程池的任务拒绝策略没有被显式配置,默认的拒绝策略是抛出一个 RejectedExecutionException。如果需要别的的拒绝策略,可以在配置中自定义
后续TODO
不知道有没有细心的小伙伴注意到,在探讨Tomcat线程池与Spring Boot默认线程池的区别时,我们发现了一个显著的差异,就是它们处理请求的方式。让我们以相同的配置参数为例:核心线程数为10,最大线程数为20,任务队列容量为30。
Tomcat线程池的工作流程:
- 当请求到达时,Tomcat首先尝试使用核心线程(10个)来处理这些请求。
- 一旦核心线程全部被占用,Tomcat不会立即将新请求放入队列,而是选择直接创建新的线程。
- 线程的创建会持续进行,直到达到配置的最大线程数(20个)。
- 当所有线程(核心和非核心)都在忙碌时,新来的请求才会被放入队列中,等待处理。
- 队列的最大容量为30个任务,一旦队列满,后续的请求将面临拒绝。
Spring Boot默认线程池(JDK线程池)的工作流程:
- 与Tomcat不同,Spring Boot线程池在核心线程(10个)全忙时,会优先将新请求放入任务队列。
- 只有当队列已满(即队列中的任务数达到了30个),线程池才会开始创建新的线程。
- 新线程的创建将持续进行,直到线程数达到最大线程数(20个)。
- 如果队列满且线程数已达到最大值,新请求将被拒绝。
不过这是为什么呢?篇幅有限,详见后续文章~
- 核心线程数(core-size)= 3:
-
还没有评论,来说两句吧...