做Java后端这些年,SpringBoot项目里线程池几乎成了躲不开的必答题。不管是发短信、推送消息、批量处理数据,还是对接第三方接口,只要涉及异步操作,就得跟线程池打交道。很多人一开始觉得这玩意简单,用@Async就完事了,可真到了线上出现任务丢失、内存飙升、接口无响应的时候,才发现线程池的门道远不止一个注解——真正核心的是ThreadPoolExecutor的参数组合、阻塞队列的选型、拒绝策略的取舍,以及SpringBoot里那套从自动装配到自定义配置的完整链路。
这篇文章我以一个实际项目的视角来拆,从线程池的底层原理讲到SpringBoot中的三种落地方式,再给出一套可以直接抄的配置模板和参数计算过程,最后整理我这两年踩过的线程池坑和排查思路。内容不分八股,也不讲空泛概念,适合正在用SpringBoot做业务开发、想搞懂线程池到底怎么配怎么用的朋友,同样适合面试前想系统梳理这块知识的人。
1. 线程池的核心概念:为什么SpringBoot项目离不开线程池
1.1 线程池到底在解决什么问题
先说个最直白的事。如果我们收到一个请求就new Thread(...)开一个线程去处理,在低并发下确实没感觉,但一旦请求量上来,线程的创建和销毁本身就会消耗大量CPU和内存资源。线程创建需要分配栈空间、做系统调用,销毁还要触发垃圾回收、释放资源,这个开销在频繁请求的场景下是很吓人的。
更麻烦的是,线程数量一旦失控,CPU会在大量线程之间频繁切换上下文,反而把宝贵的计算时间浪费在调度上,系统吞吐量不升反降。线程池的底层逻辑,就是复用一个固定数量的线程来循环处理任务队列中的任务,核心作用可以总结成三点:复用线程、控制并发上限、削峰填谷。第一点省掉了频繁创建销毁的开销,第二点防止线程无限增长拖垮系统,第三点相当于给突发的流量加了一个缓冲——任务先排队,线程慢慢处理。
在SpringBoot项目里,线程池的作用被进一步放大。因为SpringBoot的Web层本身就跑在Tomcat之类的容器线程上,如果业务逻辑里有比较耗时的IO操作(调用外部接口、读写数据库、上传下载文件等)全部直接同步执行,那一个请求就会占住一个Tomcat工作线程很长时间,容器线程池很快被耗尽,表现为整个应用“卡死”。把耗时的部分丢给独立的业务线程池异步处理,让Web线程快速返回,这是SpringBoot项目里线程池最主要的用武之地。
1.2 ThreadPoolExecutor七个核心参数逐个拆
Java里线程池的底子就是ThreadPoolExecutor,SpringBoot无论怎么封装,最终都绕不开它的七个构造参数。这七个参数决定了线程池的一切行为,必须一个个说清楚。
| 参数 | 作用 | 类比理解 | 注意事项 |
|---|---|---|---|
| corePoolSize | 核心线程数,即使空闲也保留的线程数量 | 门店的固定员工 | 并不是越多越好,要依据CPU/IO密集程度定 |
| maximumPoolSize | 线程池能容纳的最大线程数 | 忙时临时加雇的人 | 必须大于等于corePoolSize |
| keepAliveTime | 非核心线程空闲存活时间 | 临时工闲多久会被辞退 | 默认只对超出核心数的线程生效 |
| unit | keepAliveTime的时间单位 | 秒/分/毫秒 | 一般用毫秒或秒 |
| workQueue | 任务等待队列,存放下不去的任务 | 门店门口的排队区 | 队列类型直接决定线程池的行为上限 |
| threadFactory | 线程工厂,用来给线程命名、设置是否守护线程 | 给员工做工牌 | 建议自定义,是排查问题的关键手段 |
| handler | 拒绝策略,队列和最大线程都满了怎么办 | 排队都没位置时的应对方案 | 默认是AbortPolicy直接抛异常 |
这里特别提醒一下,很多人容易忽略threadFactory。我在项目里要求团队所有线程池必须自定义线程工厂,给线程起个有意义的名字,比如order-sync-pool-1、push-task-pool-1。这样线上出了问题,用jstack一抓,线程栈里清清楚楚能看到是哪类任务导致的,比对着pool-1-thread-1猜半天省事太多。
1.3 线程池的工作流程:提交一个任务后发生了什么
搞懂流程比背参数更重要。当一个任务通过execute()或者submit()提交给线程池,执行的判定顺序是这样的:
- 如果当前工作线程数小于corePoolSize,会新建一个核心线程来执行任务,而不是把任务丢进队列。
- 如果核心线程已经满了,新任务会尝试放入阻塞队列中等待。
- 如果队列也满了,线程池才会继续创建新线程,直到线程数达到maximumPoolSize。
- 如果线程数已经到上限,队列也满了,就会走拒绝策略。
这个顺序是线程池最关键的行为逻辑,很多人以为线程池是先“扩线程”再“进队列”,实际上恰恰相反。线程池的策略是先用核心线程,再缓冲区,再到非核心线程。这里隐藏了一个很多人踩过的坑:如果你用一个无界队列(比如默认的LinkedBlockingQueue),那么第3步和第4步永远不会触发,因为队列永远装不满,线程数永远停在corePoolSize。你以为配置了maximumPoolSize,实际上线程池根本没有机会扩容。
2. SpringBoot中线程池的主流落地方式
2.1 方式一:手动创建ThreadPoolExecutor
最直接的方式,不需要SpringBoot提供任何额外能力,直接new一个ThreadPoolExecutor放在某个管理类里。
@Configuration public class ThreadPoolConfig { @Bean(name = "commonThreadPool") public ThreadPoolExecutor commonThreadPool() { return new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new CustomThreadFactory("common-pool"), new ThreadPoolExecutor.CallerRunsPolicy() ); } }这种方式的优点是简单、可控、不依赖Spring的代理机制,适合项目里只需要一个全局线程池、业务比较单一的场景。但缺点也很明显:如果业务变多,各种任务混在同一个池子里,互相之间会抢线程。比如订单处理任务把线程占满了,短信推送就排队等半天,这就是典型的“池子大锅饭”问题。所以这种方式更适合小项目或临时需求,不建议在复杂业务里搞一个“上帝线程池”。
2.2 方式二:@Async + 自定义ThreadPoolTaskExecutor
SpringBoot官方推荐的异步方式是用@Async注解配合ThreadPoolTaskExecutor。ThreadPoolTaskExecutor是Spring对ThreadPoolExecutor的封装,增加了Spring生命周期管理和更友好的配置方式。
第一步,在启动类或者配置类上开启异步功能:
@EnableAsync @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }第二步,定义一个ThreadPoolTaskExecutor的Bean:
@Configuration public class AsyncConfig { @Bean(name = "taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(200); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("async-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } }第三步,在需要异步执行的方法上加@Async注解。这里有个细节必须提醒:同一个类内部调用@Async方法是不会生效的,因为异步是通过Spring的AOP代理实现的,内部调用走的是this引用而不是代理对象。所以要么把异步方法放到另一个Bean里,要么注入自身代理对象,否则方法会同步执行,坑了很多人。
@Service public class OrderService { @Async("taskExecutor") public void sendOrderNotice(Order order) { // 执行短信发送、站内信通知等耗时操作 } }@Async后面可以指定Bean名称,如果你配置了多个线程池,可以根据任务类型选择不同的池子。不指定的话,Spring会找唯一的ExecutorBean,找不到还会回退到SimpleAsyncTaskExecutor,这个回退是另一个大坑:它每次执行都会新建线程,完全不复用,高并发下会无限创建线程。
2.3 方式三:配置文件外部化,用@ConfigurationProperties绑定参数
真正到项目里,尤其是有多套环境(dev/test/prod)的团队,线程池参数应该放在application.yml里,而不是写死在Java代码中。这样调整参数不需要重新编译发版,直接改配置重启就行。
先定义参数绑定类:
@Component @ConfigurationProperties(prefix = "thread-pool") public class ThreadPoolProperties { private int corePoolSize = 4; private int maxPoolSize = 8; private int queueCapacity = 200; private int keepAliveSeconds = 60; private boolean waitForTasksToCompleteOnShutdown = true; private int awaitTerminationSeconds = 30; // 省略getter/setter }然后在配置类里读取,构建线程池Bean:
@Configuration public class ThreadPoolConfig { @Bean(name = "bizThreadPool") public ThreadPoolTaskExecutor bizThreadPool(ThreadPoolProperties props) { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(props.getCorePoolSize()); executor.setMaxPoolSize(props.getMaxPoolSize()); executor.setQueueCapacity(props.getQueueCapacity()); executor.setKeepAliveSeconds(props.getKeepAliveSeconds()); executor.setThreadNamePrefix("biz-pool-"); executor.setWaitForTasksToCompleteOnShutdown(props.isWaitForTasksToCompleteOnShutdown()); executor.setAwaitTerminationSeconds(props.getAwaitTerminationSeconds()); executor.initialize(); return executor; } }对应的application.yml配置:
thread-pool: core-pool-size: 4 max-pool-size: 8 queue-capacity: 200 keep-alive-seconds: 60 wait-for-tasks-to-complete-on-shutdown: true await-termination-seconds: 30这种方式的优势在于,生产环境发现线程池不够用,改配max-pool-size就能线上调整,配合配置中心甚至能做到不重启动态刷新(配合@RefreshScope或者Spring Cloud Config)。这也是我目前在项目里推荐的做法,兼顾了灵活性和可维护性。
3. 实战:一套线程池配置的完整演练
3.1 先从需求反推参数:什么时候该要多大线程池
线程池参数没有万能答案,必须结合业务场景去算。我以一个常见的业务为例:订单创建成功后,需要同步订单信息到ERP系统、发送短信/站内信通知用户、把订单数据写入Elasticsearch用于搜索。这三个任务都属于IO密集型,因为大部分时间都在等外部接口响应、写消息队列、操作数据库。
CPU密集和IO密集的线程数计算公式不算复杂,但很实用:
- CPU密集型:
核心线程数 = CPU核数 + 1 - IO密集型:
核心线程数 = CPU核数 * 2,更严谨一些可以用CPU核数 / (1 - 阻塞系数),阻塞系数一般在0.8到0.9之间
我的服务器是4核8线程的,按经验公式来:
corePoolSize = 4 * 2 = 8(保守估计) corePoolSize = 4 / (1 - 0.9) = 40(高阻塞场景)实际落地的时候我没有直接取某个极端值,而是取了个中间偏保守的数:核心线程8、最大线程16。原因是这些异步任务虽然阻塞多,但下游系统(ERP、短信网关)有自己的承载力,线程数翻太大反而会把下游打挂。线程池的一个重要职责是保护下游,不是把本机性能榨干。上线后用压测验证,再根据结果微调,这比理论公式更重要。
队列容量这一项,我选择的是LinkedBlockingQueue,容量设为200。为什么不是更大?如果队列容量设成几万甚至无界,突发流量来的时候任务全部积压在本地内存里,一方面可能撑爆堆内存,另一方面任务延迟越来越大,等用户都超时了任务还没被执行,就没有意义了。200这个值是在“削峰能力”和“延迟可控”之间做的平衡。
3.2 完整代码:从配置类到业务封装的落地
完整配置类代码如下,包含自定义线程工厂和拒绝策略,这个配置我可以直接复制到项目里跑:
@Configuration @EnableAsync public class ThreadPoolConfig { @Bean(name = "orderAsyncPool") public ThreadPoolTaskExecutor orderAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程数:系统可用核数的两倍 executor.setCorePoolSize(8); // 最大线程数 executor.setMaxPoolSize(16); // 队列容量 executor.setQueueCapacity(200); // 空闲线程存活时间 executor.setKeepAliveSeconds(60); // 线程名前缀,便于排查 executor.setThreadNamePrefix("order-async-"); // 拒绝策略:由调用者线程执行被拒绝的任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 优雅关闭,等待任务完成 executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } @Bean(name = "pushAsyncPool") public ThreadPoolTaskExecutor pushAsyncPool() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(500); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("push-async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }这里我特意配了两个池子:订单处理池和推送池。如果只有一个池子,订单高峰期的大量订单任务就会把推送任务堵住,导致用户下单后收不到通知。所以当业务里任务优先级、耗时特征差异明显时,一定要拆池子。taskExecutor.initialize()这段我经常看到有人忘写,导致的后果是Spring在启动时可能因为Bean初始化顺序问题报错,建议手动调用一次。
然后定义异步任务的Service层,把业务封装好:
@Service public class OrderAsyncService { @Async("orderAsyncPool") public void syncOrderToErp(Order order) { // 调用ERP接口同步订单 erpClient.syncOrder(order); } @Async("pushAsyncPool") public void sendUserNotify(Order order) { // 发送短信消息 smsClient.send(order.getPhone(), "您的订单已提交"); } }这样在Controller或者订单Service里调用的时候,就变成了一句简单的:
orderAsyncService.syncOrderToErp(order); orderAsyncService.sendUserNotify(order);调用一返回,代码就要往下走,但在日志里能看到这两个方法实际是在不同线程里执行完成的。
3.3 优雅关闭:别让线程池在应用停机时丢任务
SpringBoot应用在重启、发布、缩容时,如果直接杀掉进程,线程池里的任务可能才执行到一半,数据就丢了。很多团队不会注意这个问题,直到某次凌晨发布后客户反馈“昨晚的订单短信没收到”,才意识到是停机时任务被粗暴中断了。
配置里有两个关键参数专门解决这个问题:
executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30);waitForTasksToCompleteOnShutdown=true表示在容器关闭时,线程池会等待所有任务执行完成再关闭线程;awaitTerminationSeconds=30最多等待30秒,防止某些任务一直卡住导致停机无限延迟。这两个组合是把“把该干的活干完”和“不能无限等下去”之间的折中。
如果是手动创建的ThreadPoolExecutor,容器销毁时不会自动关闭,需要在@PreDestroy里手动调用:
@PreDestroy public void destroy() { threadPoolExecutor.shutdown(); try { if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) { threadPoolExecutor.shutdownNow(); } } catch (InterruptedException e) { threadPoolExecutor.shutdownNow(); Thread.currentThread().interrupt(); } }shutdown()和shutdownNow()的区别是:前者温柔地停止接收新任务,已经提交的任务继续执行;后者直接尝试中止正在运行的任务,未执行的任务队列直接清空。一般先用shutdown()再配合awaitTermination等待,超时了才强制关闭。
4. 阻塞队列选择与拒绝策略的取舍
4.1 三种常用阻塞队列怎么选
注意:在线程池源码里,队列是
BlockingQueue<Runnable>,SpringBoot封装后虽然队列容量变成数字参数,但底层还是一个有界LinkedBlockingQueue。
线程池的队列选择是决定整个池子行为的关键,这个点是网上资料说得最零散、也最容易误导人的地方。我做了一张对比表,直接把结论摆出来:
| 队列类型 | 是否有界 | 数据结构 | 适用场景 | 主要风险 |
|---|---|---|---|---|
| ArrayBlockingQueue | 有界 | 数组 | 需要严格控制队列长度 | 必须设置容量,否则默认容量为1,容易误触发创建线程 |
| LinkedBlockingQueue | 有界/无界(取决于构造方法) | 链表 | 默认无界,几乎不用设置 | 无界时队列永不占满,maximumPoolSize形同虚设 |
| SynchronousQueue | 无容量 | 不存储元素 | 直接交接给线程执行,适合任务少且执行快的场景 | 配合maximumPoolSize,否则任务会频繁被拒绝 |
| DelayQueue | 无界 | 延迟队列 | 延迟任务的场景 | 暂停、调度需求的复杂度高 |
| PriorityBlockingQueue | 无界 | 堆 | 需要任务优先级排序 | 无界,且不能配合SynchronousQueue使用 |
4.2 为什么“无界队列”容易埋雷
每一种队列我都用过,其中最危险的是无界的LinkedBlockingQueue。它在构造时不传容量,默认值相当于Integer.MAX_VALUE,也就是一个“装不满”的队列。造成的直接后果是线程池永远不会创建超过corePoolSize的线程,非核心线程数变成摆设。
之前有个朋友的项目就栽在这上面。他们给线程池设了core=10、max=100,但队列用的无界LinkedBlockingQueue,结果某天接口被刷,任务量暴增,10个核心线程忙不过来,队列里积压了几十万个任务,内存直线飙升,应用频繁Full GC,最后直接OOM。排查了半天,才发现他们以为“队列无限长就不用担心拒绝”,实际是把内存风险全扛在了自己身上。
所以我的建议很简单:生产环境一定要用有界队列,容量根据业务量评估,设置容量本身就是一种“流量控制”。如果担心队列满后任务被拒,用后面的拒绝策略兜底,比用无界队列“硬吃”要安全得多。
4.3 四种拒绝策略实例分析
队列满了,线程也满了,这时候新提交的任务就要走拒绝策略。JDK内置了四种:
| 策略 | 行为 | 风险 | 我的推荐度 |
|---|---|---|---|
| AbortPolicy(默认) | 直接抛RejectedExecutionException | 若没捕获,业务可能中断 | 一般,除非你对异常有兜底 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 增加调用线程负担,可能拖慢调用方 | 推荐,削峰保底 |
| DiscardPolicy | 直接丢弃,静默 | 任务丢得悄无声息 | 不推荐 |
| DiscardOldestPolicy | 丢弃队列中最旧的任务,再提交 | 可能丢弃重要数据 | 不推荐,除非明确知道能丢 |
实际项目中我用得最多的是CallerRunsPolicy。它的核心逻辑是:线程池处理不过来的时候,把任务“弹回”调用方所在的线程去执行。比如你在Controller线程里提交了一个异步任务,线程池满了没处放,那就由Controller的线程自己把这个任务执行了。这样任务不会丢,同时给调用方一个“天然限流”的信号,让上游感受到处理压力,从而自动降低提交频率。
这听起来会多浪费一点请求线程,但相比静默丢任务或者直接抛异常导致业务流程中断来说,是最平衡的方案。当然如果任务对延迟极其敏感,宁可丢也不能阻塞调用方,那可以考虑其他策略。如果内置策略都不满足需求,也可以自己实现RejectedExecutionHandler接口,把被拒绝的任务持久化到数据库或写入MQ,后面再补偿重试——这是金融类项目常见的兜底方案,本质上是在“在线处理”和“离线补偿”之间做权衡。
5. 常见问题与排查技巧
5.1 线程池监控:三个维度看清运行状况
配置再好,线上看不到运行状态都是瞎猜。我维护的项目里,线程池一定是暴露监控指标的,至少包含三个核心指标:
- 当前活跃线程数(activeCount)
- 队列中待处理任务数(queueSize)
- 已完成任务数和被拒绝任务数(taskCount、rejectedCount)
如果是SpringBoot 2.x+Actuator+Micrometer,可以接入Prometheus,Grafana里直接画面板。如果项目比较简单,也可以自己在池子外层包一层,定时打印日志:
@Component public class ThreadPoolMonitor { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @PostConstruct public void startMonitor() { scheduler.scheduleAtFixedRate(() -> { ThreadPoolExecutor pool = getOrderPool(); int activeCount = pool.getActiveCount(); int poolSize = pool.getPoolSize(); long queueSize = pool.getQueue().size(); long taskCount = pool.getTaskCount(); if (activeCount >= poolSize || queueSize > 100) { log.warn("[ThreadPoolMonitor] order pool active={}, poolSize={}, queue={}, task={}", activeCount, poolSize, queueSize, taskCount); } }, 10, 30, TimeUnit.SECONDS); } }很多线上问题不是突然爆发的,而是缓慢恶化的。比如队列长度每天涨一点,可能持续一周才达到瓶颈。如果监控只报警不记录趋势,就只能等出大事才反应过来。有了队列积压的监控曲线,提前一周就能发现隐患。
5.2 典型故障排查实录
每次帮人定位线程池问题,翻来覆去基本都是几个经典套路。
故障一:异步任务“丢”了。现象是日志里没有报错,但某些定时任务或者异步方法偶尔不执行。排查后发现是@Async方法自调用导致的——方法内部调用同一个类的异步方法,走了this引用,AOP代理没生效,变成同步执行还不报错。这个只能从代码规范上避免,并配合在异步方法里打印日志,写清楚入口线程名,就很容易看出是否真的异步了。
故障二:应用响应越来越慢,线程数一直在涨。用jstack抓线程栈,发现大量线程阻塞在某次HTTP调用的等待响应上,对方接口已经假死,所有线程都趴在连接上等超时。这时候线程池设置再大也没用,本质是下游故障传导到了上游。处理方式是给线程池里的任务设置超时(比如用Future.get的超时参数),并且在上游加熔断(比如Sentinel、Resilience4j),别让一个下游故障把整个应用拖死。
故障三:生产者突然大量提交,触发拒绝策略。我们用CallerRunsPolicy,所以不会抛异常,但Controller线程被占住,接口RT飙升。排查发现是某个定时任务一次性捞了十万条数据批量提交线程池,触发回压后单线程执行变慢。最终把批量提交改成“按页提交+限速”,每次提交1000条,间隔几十毫秒,问题解决。线程池不背这个锅,是上游提交节奏太粗暴了。
5.3 避坑建议清单
最后把这几年踩过、看过、帮别人解决的线程池坑汇总在一起,按优先级排个序:
- 不要用
Executors自带的静态方法。newFixedThreadPool和newSingleThreadExecutor用的是无界队列,newCachedThreadPool最大线程数是Integer.MAX_VALUE。这三种都容易在突发场景下出事,这也是阿里Java开发手册把这条列为强制的核心理由。 - 异步任务必须显式捕获异常。
Runnable里抛出的异常不会自动打印,如果你用submit()提交,异常还只存在Future对象里,不调用future.get()根本看不到。最好的做法是在任务里try-catch并打日志,或者设置全局的UncaughtExceptionHandler。 - 线程池参数要在上线前压测。按公式算出来的参数只是起点,一定要结合业务流量压测。常见压测结果是“核心线程不够用”或者“队列太小频繁触发拒绝”,调整后再上线。
- 拆池子比超大池子更靠谱。按业务类型拆多个线程池,即使某个业务的池子炸了,也不会影响其他核心业务。很多人图省事搞一个“万能大池子”,结果波及全站。
- 注意线程上下文传递。在线程池里处理任务时,主线程的
ThreadLocal(比如登录用户、TraceId)默认是传不过去的。如果业务里需要透传,可以用TransmittableThreadLocal(阿里开源的TTL)或者手动在提交任务时把上下文塞进任务对象里。 - 别在异步任务里做事务操作。
@Transactional和@Async叠加时,事务是绑定在异步线程上的,不跟调用方共享。别以为方法A里调用了异步方法B,B的事务就能纳入A的统一管理,连不上。
我在实际项目中还有个小习惯:每次提交线程池任务时,在任务的开始和结束都打印一下线程名和队列情况。这样线上出了问题,从日志里就能还原出当时的线程池水位,排查效率翻倍。
线程池这东西,配置好了没什么存在感,配置差了就是定时炸弹。把这篇文章里的参数计算、队列选型、拒绝策略和排错思路吃透,再结合自己项目的实际压力测一测,线上基本不会踩大坑。如果你正在做SpringBoot项目,建议先把线程池监控接起来,哪怕只是打印日志,都算是往前迈了一大步。