手写 ThreadPoolExecutor 避坑:corePoolSize 设 CPU 核数+1(CPU 密集)或 2×核数(IO 密集),maximumPoolSize 与 core 相等或差 2–4 倍,keepAliveTime 用 60L 秒,workQueue 必选有界 ArrayBlockingQueue(容量 100–1000),threadFactory 要自定义命名,拒绝策略优先 CallerRunsPolicy 或自定义落库/Kafka,shutdown 后必调 awaitTermination,监控 activeCount、queue.size 和积压量并上报 Prometheus。

ThreadPoolExecutor 构造参数怎么填才不踩坑
直接用 Executors 工具类创建线程池(比如 newFixedThreadPool)在生产环境基本等于埋雷——它用的 LinkedBlockingQueue 无界队列,任务积压时会 OOM。必须手写 ThreadPoolExecutor,关键在五个参数的取舍:
-
corePoolSize:常驻线程数,建议设为 CPU 核数 + 1(CPU 密集型)或2 × CPU 核数(IO 密集型),别硬写 10/20 -
maximumPoolSize:最大线程数,和corePoolSize相等可避免动态扩容(更可控);若需弹性,差值不宜超过 2–4 倍,否则线程上下文切换开销反超收益 -
keepAliveTime:非核心线程空闲存活时间,设为60L秒足够,别用 0 或 Long.MAX_VALUE -
workQueue:必须用有界队列,ArrayBlockingQueue最稳妥(容量建议 100–1000,看任务平均耗时和吞吐预估) -
threadFactory:一定要自定义,至少带上业务前缀,比如new NamedThreadFactory("order-processor"),不然线程 dump 里全是pool-1-thread-1,查问题抓瞎
拒绝策略(RejectedExecutionHandler)选哪个
默认的 AbortPolicy 直接抛 RejectedExecutionException,但多数业务不能丢任务。常见选择:
-
CallerRunsPolicy:让提交线程自己执行任务,适合突发流量可接受延迟的场景(比如后台报表生成) - 自定义策略:把任务写入 Redis 队列或 Kafka,再由消费者重试(注意幂等)
- 绝对别用
DiscardPolicy或DiscardOldestPolicy,除非你明确知道丢的是什么、且后果可控
设置方式:
new ThreadPoolExecutor(..., new ThreadPoolExecutor.CallerRunsPolicy())
线程池 shutdown 和 shutdownNow 的区别与时机
不是所有地方都能调 shutdown(),尤其 Spring 管理的 Bean:
立即学习“Java免费学习笔记(深入)”;
-
shutdown():停止接收新任务,等已提交任务执行完再关闭;适合应用优雅停机(配合 Spring@PreDestroy) -
shutdownNow():尝试中断所有正在执行的线程,并返回未执行的任务列表;慎用——中断不一定生效(比如任务里没响应Thread.interrupted()),还可能引发资源泄漏 - 关键点:调用
shutdown()后,必须跟awaitTermination()等待结束,超时后视情况决定是否shutdownNow(),别只调 shutdown 就不管了
监控线程池状态的实用方法
光靠日志看不出线程池是否健康,得主动采集指标:
- 用
getActiveCount()查当前忙线程数,持续接近corePoolSize说明配置偏小 -
getQueue().size()要盯住,持续 > 队列容量 70% 就该告警 -
getCompletedTaskCount()和getTaskCount()做差,能算出积压任务量 - 别依赖 JMX 暴露的全部指标——有些字段(如
largestPoolSize)是历史峰值,对实时决策没用
这些值建议每 10 秒打点上报到 Prometheus,配合 Grafana 看曲线比看单次日志有用得多。










