
线程数设成 CPU 核心数的几倍才合理
没有放之四海而皆准的“倍数”,盲目套用 2× 或 4× CPU核心数 很容易拖垮吞吐或浪费上下文切换。关键看任务类型:纯计算型任务,线程数 ≈ Runtime.getRuntime().availableProcessors() 最稳妥;I/O 密集型(如数据库查询、HTTP 调用)才需要更多线程来“填满等待空隙”。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 先用
jstack或VisualVM观察线程实际阻塞占比——如果WAITING/TIMED_WAITING状态线程长期占 70% 以上,说明当前线程数不足 - 对 HTTP 客户端(如
OkHttp)、数据库连接池(如HikariCP),线程数要和它们的并发上限匹配,否则线程再多也卡在连接池上 - 别忽略 GC 压力:线程数翻倍后
Full GC频次明显上升,往往意味着堆内存分配速率暴增,该调-Xmn而不是加线程
ThreadPoolExecutor 构造参数怎么配才不踩坑
很多人只调 corePoolSize 和 maximumPoolSize,却忽略了 keepAliveTime 和 workQueue 的隐性耦合。比如用无界队列 LinkedBlockingQueue 时,maximumPoolSize 实际失效;而用 SynchronousQueue 又极易触发拒绝策略,除非你明确想让新任务立即失败。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 计算密集型:用
corePoolSize = maxPoolSize = CPU核心数,workQueue选SynchronousQueue,避免任务堆积 - I/O 密集型:
corePoolSize设为估算的平均并发请求数(如 50),maxPoolSize可略高(如 100),workQueue用有界队列(如ArrayBlockingQueue(100)),防 OOM -
keepAliveTime别设 0L——哪怕只设 60L,也能避免空闲线程瞬间销毁又重建的抖动
为什么 Executors.newFixedThreadPool(10) 在生产环境很危险
它底层用的是无界 LinkedBlockingQueue,意味着只要任务提交速度 > 消费速度,队列会无限增长,最终 OutOfMemoryError: Java heap space。这不是理论风险,是线上高频事故点。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 永远不用
Executors工厂方法创建线程池,一律手写ThreadPoolExecutor构造,显式控制队列容量和拒绝策略 - 拒绝策略优先选
ThreadPoolExecutor.CallerRunsPolicy:让调用线程自己执行任务,天然限流,比直接丢弃或抛异常更可控 - 给线程池命名(通过
ThreadFactory),否则jstack里全是pool-1-thread-1,根本分不清是谁家的池子
如何验证当前线程数设置是否真有效
看 ThreadPoolExecutor.getCompletedTaskCount() 和 getActiveCount() 的比值没用——它们反映的是瞬时状态,掩盖了排队延迟和资源争用。真正要盯的是端到端指标:P99 响应时间是否稳定、下游服务错误率是否上升、GC pause 是否变长。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 压测时固定 QPS,逐步增加线程数,画出“线程数 vs P99 延迟”曲线——拐点之后延迟反弹,就是过载临界点
- 监控
ThreadPoolExecutor.getQueue().size(),持续 > 队列容量 70%,说明消费不过来,要么优化单任务耗时,要么扩容下游依赖 - 注意 JVM 参数干扰:开了
-XX:+UseParallelGC却配了 50 个线程,GC 线程本身就会抢走大量 CPU,导致业务线程饥饿
线程数不是调参游戏,它是 CPU、内存、I/O、GC、下游依赖五者博弈的结果。最容易被忽略的,是把“线程池大小”当成孤立变量去调,而忘了它一动,整个链路的等待、排队、竞争关系全在变。










