线程池不是万能药,用不好照样出事
有次上线后突然报警,系统响应慢得像卡带的老式录像机。查了一圈才发现,是某个服务每秒创建几十个新线程处理请求,JVM直接被拖垮。后来把这部分改成线程池,问题立马缓解。这事儿让我意识到,光会用线程池不行,得知道它背后是怎么跑起来的。
线程池的核心结构长什么样
线程池本质上是个“任务调度中心”。你把任务扔进去,它安排空闲线程去执行。核心部件就几个:工作线程集合、任务队列、拒绝策略、线程管理规则。
比如 Java 里的 ThreadPoolExecutor,构造函数一堆参数:核心线程数、最大线程数、空闲时间、任务队列、拒绝策略。这些不是凑数的,每个都对应实际运行时的行为控制。
任务进来后怎么流转
新任务来了,先看有没有空闲的核心线程。有就直接执行。没有的话,就把任务塞进阻塞队列等著。这时候如果队列满了,才会考虑开新线程,直到达到最大线程数。再超了,就触发拒绝策略——可能是抛异常,也可能是丢弃任务。
这个流程看着简单,但线上很多超时问题,都是因为队列太长,任务在里头排队排了几百毫秒甚至更久。
线程是怎么被复用的
每个工作线程启动后不会立刻退出。它们进入一个死循环,不断从任务队列里 take() 取任务。取到了就 run(),执行完继续取。这就是线程复用的关键:线程本身一直活着,只是不断换任务跑。
while (running) {
Runnable task = workQueue.take();
if (task != null) {
task.run();
}
}
注意这里用的是 take() 而不是 poll(),意味着队列没任务时线程会阻塞住,不消耗 CPU。这也是为啥线程池比频繁创建线程省资源。
常见坑点:队列选错出大事
有人图省事用无界队列,比如 LinkedBlockingQueue 不设上限。短期看没问题,但流量一冲上来,任务越堆越多,内存直接撑爆。之前有个服务就是因为用了无界队列,突发批量导入导致 OOM,整个应用挂掉。
还有一种情况是核心线程数设成 0,又用了 SynchronousQueue。这种组合下,除非有线程正在等着接活,否则任务直接被拒绝。结果就是并发稍高一点,大量请求失败。
拒绝策略不能忽视
默认的 AbortPolicy 是直接抛 RejectedExecutionException。如果你没捕获,上层调用就炸了。线上有些接口莫名其妙返回 500,追到底层发现就是这儿炸的。
更稳妥的做法是自定义拒绝策略,比如记录日志、写入补偿队列,或者通知监控系统。
动态调整真的安全吗
有人想着“我能不能运行时调大线程数”,于是调 setMaximumPoolSize()。但要注意,这个值只是上限,真正起线程还得看队列状态。而且频繁修改可能引发线程震荡——一会儿起一堆,一会儿又回收,上下文切换反而拖慢性能。
真要动态调参,建议结合监控指标,比如队列长度、活跃线程数,做平滑调整,别拍脑袋改。
排查这类问题该看什么
遇到线程相关故障,先抓线程栈:jstack 或 arthas 都行。看看是不是一堆线程卡在某段逻辑上,或者大量线程处于 WAITING 状态干等任务。
再查任务队列长度和拒绝任务数。这两个指标突增,基本就能定位到是线程池配置或流量异常的问题。
最后别忘了看 GC 日志。任务堆积太多,对象创建频繁,容易引发频繁 Full GC,形成恶性循环。