1-线程池
线程池 = 线程的"复用工厂 + 任务调度系统",只是如何用有限线程,稳定地处理无限任务
2-为什么一定要线程池
当你不用线程池的时候:
- 每个任务一个线程(
new Thread),后果:任务多 → 线程多 → 上下文切换爆炸 / 内存耗尽 / OOM - 少量线程串行,后果:吞吐低 / 延迟高 / 系统假死
使用线程池只是在吞吐、延迟、资源之间做一个平衡
3-具体实现
使用的类为:
java
java.util.concurrent.ThreadPoolExecutor
构造参数:
java
public ThreadPoolExecutor(
int corePoolSize,// 核心线程数
int maximumPoolSize,// 最大线程数
long keepAliveTime,// 存活时间
TimeUnit unit,// 时间单位
BlockingQueue<Runnable> workQueue,// 任务队列
ThreadFactory threadFactory,// 线程创建时的规则
RejectedExecutionHandler handler// 拒绝策略
)
构造参数释义:
-
corePoolSize(核心线程数):线程池"常驻"的线程数量
-
workQueue(任务队列):决定线程池的行为
常见队列类型为:
队列 特点 后果 LinkedBlockingQueue 无界 OOM风险 ArrayBlockingQueue 有界 可控 SynchronousQueue 不存任务 压力直接给线程 PriorityBlockingQueue 优先级 顺序复杂 -
maximumPoolSize(最大线程数):线程池允许创建的最大线程数量
-
keepAliveTime(存活时间):非核心线程,空闲多久会被回收
-
RejectedExecutionHandler(拒绝策略):
常见策略为:
策略 行为 AbortPolicy 抛异常(默认) CallerRunsPolicy 调用方线程执行(常用) DiscardPolicy 直接丢弃 DiscardOldestPolicy 丢队列最老任务 -
threadFactory(线程的生产规则):
java// 线程池自己并不直接 new Thread(),会通过下方代码创建线程: threadFactory.newThread(runnable) // threadFactory 最重要的 4 个作用 new ThreadFactoryBuilder() // 1.线程命名 .setNameFormat("order-exec-%d") // 自动生成 order-exec-0, order-exec-1... // 2.设置是否为守护线程:一般主要的业务用,设置为false .setDaemon(false) // 3.设置优先级(使用较少) .setPriority(Thread.NORM_PRIORITY) // 4.设置 UncaughtExceptionHandler:防止线程因异常而终止但不输出任何信息,处理未捕获的异常 .setUncaughtExceptionHandler((t, e) -> { log.error("线程 {} 异常", t.getName(), e); });
4-线程池特点
-
单个线程池实例,全局只初始化一次
-
线程池的数量全局不止一个:可以分出很多个,比如:定时任务线程池,web 容器线程池(Tomcat)等等。主要是控制
允许多少个任务同时处于运行态/可运行态,设计线程池的大小需要根据设备为准 -
池的大小分为两种类型:
-
CPU密级型:很少阻塞、一直占CPU;线程基本都在使用cpu,所以线程数量基本契合cpu的配置即可
线程数 ≈ CPU 核数 或 CPU 核数 + 1 比如: 12 核 16 线程的服务器 设置:12 ~ 13 -
IO密集型:大量等待、CPU空闲;所以线程数量多一些,尽量高效提高cpu利用率,但不能太多
线程数 ≈ CPU核数 × (1 + IO等待时间 / 计算时间) 简化:CPU 核数 × 2 ~ 4 比如: 12 核 16 线程的服务器 设置:24 ~ 48
-
5-线程池一般的分类
| 类型 | 是否 CPU 密集 | 示例 |
|---|---|---|
| Web 请求 | IO | Controller |
| 业务异步 | 混合 | @Async |
| 定时任务 | IO / 混合 | @Scheduled |
| CPU 密集 | 是 | 加解密 |
6-Spring内部存在的线程池
6-1.Web容器线程池(最重要)
#Tomcat(默认)
server.tomcat.threads.max = 200
特点:
- IO密集型
- 每个HTTP请求一个线程
- 线程大多数时间在等DB/Redis
6-2.定时任务线程池
java
@Scheduled
默认:
- 单线程
- 所有定时任务串行
一般必须自己配:
TaskScheduler
6-3.@Async线程池
java
@Async
// 每次 @Async 都可能新建一个线程,在默认配置下,本质行为 ≈ new Thread()
默认:
SimpleAsyncTaskExecutor- 不是真正线程池
- 会无限创建线程
生产环境必须自定义
PS:Redis/DB等外部中间件连接池的设置大小尽量要契合,关注 max-active 等连接数参数,确保它能匹配你的业务并发量
7-自己建立线程池的条件
凡是:异步、定时、非 HTTP 主流程都应该用"自定义的线程池"
8-新项目线程池调整
一个新的spring项目,有一些情况下需要调整
8-1.Tomcat线程池
默认情况:
yaml
maxThreads = 200
minSpareThreads = 10
#Tomcat 线程数 ≈ 下游连接池总容量 × 1~2
示例:
yaml
server:
tomcat:
threads:
max: 200 # 最大工作线程数
min-spare: 20 # 最小空闲线程
max-connections: 10000 # 最大连接数(含等待)
accept-count: 100 # 等待队列长度
connection-timeout: 20000 # 连接超时(ms)
一般经验:
yaml
# CPU密集型应用(计算为主)
最大线程数 ≈ CPU核心数 × (1~2)
# IO密集型应用(数据库、网络调用多)
最大线程数 ≈ CPU核心数 × (2~4) × (1 + 等待时间/处理时间)
# 混合型(通用经验值)
最大线程数 = CPU核心数 × 4
最小空闲线程 = CPU核心数 × 2
# 1核2G(低配)
server:
tomcat:
threads:
max: 50-100
min-spare: 5-10
# 2核4G(标准)
server:
tomcat:
threads:
max: 100-150
min-spare: 10-20
# 4核8G(中配)
server:
tomcat:
threads:
max: 150-200 # 默认值刚好适合
min-spare: 20-30
# 8核16G+(高配)
server:
tomcat:
threads:
max: 200-400 # 需要调高
min-spare: 30-50
8-2.@Scheduled定时任务
默认行为:所有 @Scheduled,共用 1 个线程,串行执行,一般情况下必改
java
@Configuration
@EnableScheduling// 启用定时任务功能,允许使用@Scheduled注解
public class ScheduleConfig {
// Spring会自动使用这个自定义的调度器而不是默认的
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);// 设置线程池大小为4个线程(通常建议2-8个)
scheduler.setThreadNamePrefix("sched-");// 设置线程名称前缀,便于调试识别
scheduler.setAwaitTerminationSeconds(30);// 设置关闭时等待时间30秒
scheduler.setWaitForTasksToCompleteOnShutdown(true);// 应用关闭时等待任务执行完成
return scheduler;
}
}
8-3.@Async异步线程池
默认 @Async = 什么?
java
SimpleAsyncTaskExecutor ≈ new Thread()
自定义异步线程池:
java
@Configuration
@EnableAsync// 启用异步方法执行功能,配合 @Async 注解使用
public class AsyncConfig {
@Bean("asyncExecutor")// 将方法返回的对象注册为名为 "asyncExecutor" 的 Spring Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);// 核心线程数
executor.setMaxPoolSize(32);// 最大线程数:线程池允许的最大线程数量
executor.setQueueCapacity(500);// 队列容量:任务队列最大容量,超出后创建新线程
executor.setThreadNamePrefix("async-");// 创建的线程名称前缀,便于调试识别
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 当线程池满载时,由调用线程自己执行任务,起到缓压作用
executor.initialize();
return executor;
}
}
// 具体使用:
@Async("asyncExecutor")
public void doSomething() {
// 耗时操作将在独立线程中执行
}
线程分配策略:
- 优先使用核心线程: 初始阶段先创建16个核心线程
- 队列缓冲机制: 新任务先放入容量为500的任务队列
- 动态扩容: 队列满了才创建超过核心线程数的新线程
- 上限控制: 线程总数不超过32个
执行流程:
- 任务量 ≤ 16个 → 使用核心线程
- 任务量 > 16个 → 先排队等待
- 队列满(>500个任务)→ 创建新线程直到达到32个上限
- 达到上限后 → 调用者线程直接执行任务(拒绝策略)
8-4.数据库 / Redis
对齐连接数:
yaml
# 数据库(HikariCP)
spring:
datasource:
hikari:
maximum-pool-size: 10 ~ 20
# Redis(Lettuce)
spring:
redis:
lettuce:
pool:
max-active: 8 ~ 16
#数据库连接数 = min(数据库限制 ÷ 实例数, CPU核心数 × 10)
#Redis连接数 = CPU核心数 × 2
8-5.其他
其实正常项目还会创建一些其他的线程池,除了Spring自带的Tomcat、定时任务、异步任务的线程池,一般还会创建:
-
通用业务异步线程池,1个,用于:通知、事件、异步处理
-
外部第三方接口线程池,1~2个,特点:慢、不稳定、有并发上限
-
CPU 密集型线程池,0或1个,用于:加密、大计算、规则引擎
-
后台/低优先级线程池,1 个,用于:日志、统计、对实时性不敏感任务
9-多线程池好处
现在的系统中使用一个池,管理所有任务也是不可接受的
现在的系统一般都是使用多个线程池:
-
资源隔离:
bash如果你只有一个线程池: 第三方慢 → 线程占满 → HTTP 请求进不来 → 整个服务不可用 -------------------- 线程池隔离之后: 第三方池满 → 第三方失败 主业务照常 -
失败可控:
有界线程池+拒绝策略:返回错误、快速失败、触发降级 -
可观测性:
bash多个线程池意味着: async-biz-queue 爆了 third-party-pool 耗尽 cpu-pool 满负载 -------------- 而不是: 系统很慢,不知道为什么