线程池 → Spring 异步 → MQ 消费者 追问链#
追问路径#
Q: 线程池核心参数怎么配?
→ 7大参数:corePoolSize / maximumPoolSize / keepAliveTime / workQueue / threadFactory / rejectedExecutionHandler / unit
Q: Spring @Async用的什么线程池?
→ 默认SimpleAsyncTaskExecutor——每次创建新线程不复用!负载高时约1 thread/ms,必须自定义ThreadPoolTaskExecutor
Q: MQ消费者的并发是怎么控制的?
→ Kafka由partition数决定消费者并发上限(一个partition只能被同组一个消费者消费);RocketMQ由consumeThreadMin/Max控制
├─ Q: 消费慢导致消息积压怎么办?
│ → 扩消费者实例数(不超过partition数) + 临时增大消费线程池 + 降级跳过非核心消息
│ Q: 线程池的拒绝策略怎么选?
│ → AbortPolicy(默认, 抛异常) / CallerRunsPolicy(调用者线程执行, 有反压效果) / DiscardPolicy / DiscardOldestPolicy
│ Q: 生产中怎么选?
│ → 关键业务用CallerRunsPolicy(宁可慢不丢);日志采集用DiscardPolicy(允许丢弃);报警+降级用自定义策略
│ Q: 线程池参数能动态调整吗?
│ → JDK原生支持setCorePoolSize()/setMaximumPoolSize()热更新;可配合Nacos/Apollo动态推送
└─ Q: 线程池大小怎么定?
→ CPU密集型:核心数+1(减少上下文切换);IO密集型:核心数 / (1 - 阻塞比例),如4核+80%IO阻塞→20线程
Q: 实际怎么确定阻塞比例?
→ 压测观察:Arthas的trace命令看方法耗时分布;或用SkyWalking的span耗时分析
Q: Kafka Consumer Rebalance对消费线程有什么影响?
→ Rebalance期间所有消费者暂停消费(STW效应),触发条件:消费者增减/心跳超时/partition变化plaintext涉及知识点#
- 线程池核心参数与执行流程 — 7大参数与任务提交→核心线程→队列→最大线程→拒绝策略
- 线程池大小如何设定 — CPU密集 vs IO密集的经验公式
- 线程池拒绝策略 — 4种内置策略+自定义策略
- Spring-@Async原理 — Spring异步执行的代理机制
- 消息积压处理方案 — MQ消费端扩容与降级
- 消费者组与Rebalance — Kafka Consumer Group的再均衡
- CompletableFuture用法 — 异步编排与线程池配合
- 死锁条件与排查 — 线程池嵌套提交导致的死锁
- ThreadLocal原理与内存泄漏 — 线程池复用线程时ThreadLocal的传递问题
核心串联逻辑#
- 线程池本质:复用线程(避免创建销毁开销) + 控制并发度(防止资源耗尽) + 任务队列缓冲(削峰)
- 执行流程:提交任务 → 核心线程未满创建线程 → 满了放队列 → 队列满了创建到最大线程数 → 都满了执行拒绝策略
- Spring @Async陷阱:默认SimpleAsyncTaskExecutor不复用线程,高并发下OOM;必须配置ThreadPoolTaskExecutor
- MQ消费并发:Kafka的并发度=min(消费者数, partition数),扩消费者超过partition数无意义
- 动态调参:JDK ThreadPoolExecutor原生支持setCorePoolSize()热更新,配合配置中心实现不重启调整
- 代码示例:
java// Spring自定义线程池 (必须配置,否则@Async用SimpleAsyncTaskExecutor!) @Bean("taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(200); executor.setRejectedExecutionHandler(new CallerRunsPolicy()); return executor; }
面试回答串联#
30秒速答#
“线程池7大参数,核心是corePoolSize、workQueue和拒绝策略。Spring @Async必须自定义Executor,默认的SimpleAsyncTaskExecutor不复用线程。MQ消费并发由partition数决定上限。线程池大小CPU密集型=核心数+1,IO密集型=核心数/(1-阻塞比例)。“
2分钟展开答#
“线程池的执行流程是:任务提交后先检查核心线程数,未满则创建核心线程;满了放入工作队列缓冲;队列也满了才创建到最大线程数;最大线程数也满了执行拒绝策略。Spring @Async有个大坑——默认使用SimpleAsyncTaskExecutor,每次调用都创建新线程不复用,高并发下约1 thread/ms直接OOM,必须自定义ThreadPoolTaskExecutor。拒绝策略选型上,关键业务推荐CallerRunsPolicy——调用者线程自己执行有反压效果,宁可慢不丢数据。线程池大小经验公式:CPU密集型设核心数+1(减少上下文切换),IO密集型用核心数/(1-阻塞比例)——4核CPU+80%时间IO阻塞则设20线程。实际阻塞比例通过Arthas trace命令或SkyWalking span分析确定。MQ消费者并发度受partition数限制,Kafka同一partition只能被组内一个消费者消费,扩消费者超过partition数无意义。积压时先扩partition+消费者,临时调大消费线程数快速消化。JDK原生支持setCorePoolSize()热更新线程池大小,配合Nacos可以不重启调整。“
相关追问链#
- JVM-GC-内存泄漏-OOM追问链 — ThreadLocal在线程池中的泄漏问题(线程复用导致ThreadLocal累积)
- MQ可靠性-顺序-积压-事务消息追问链 — MQ消费端的可靠性与积压处理
- Spring-IoC-AOP-事务-循环依赖追问链 — @Async依赖AOP代理机制
- 进程线程-调度-上下文切换-协程追问链 — 线程池与上下文切换的权衡