为什么线程池总在深夜崩溃?
昨天我这项目又经历了一次爆破——路由推送服务突然崩溃,排查发现线程池队列堆积了几万任务直接把内存撑爆。早上起来看见人都麻了,线程池用不好,分分钟变系统炸弹。今天我们就来系统梳理线程池的实战技巧。
一、四大线程池类型:用错场景就是灾难
1. 单线程池:日志写入的守护者
// 保证日志顺序写入,避免多线程竞争
ExecutorService single = Executors.newSingleThreadExecutor();
single.execute(() -> System.out.println("日志1"));
single.execute(() -> System.out.println("日志2"));
// 输出顺序:日志1 → 日志2
典型翻车场景:错误用于高并发接口,请求堆积导致响应延迟飙升
2. 固定线程池:数据库连接池的好搭档
ExecutorService fixed = Executors.newFixedThreadPool(5);
// 提交100个查询任务
for(int i=0; i<100; i++){ fixed.execute(DB::query);
}
隐藏巨坑:底层使用无界队列(LinkedBlockingQueue),突发流量直接OOM
3. 缓存线程池:秒杀活动的双刃剑
ExecutorService cached = Executors.newCachedThreadPool();
// 秒杀瞬间涌入1万请求
cached.execute(() -> handleSeckillRequest());
致命问题:最大线程数=Integer.MAX_VALUE,线程爆炸耗尽CPU
4. 手动参数池:最优解决方案
int cores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor custom = new ThreadPoolExecutor( 2 * cores, // 核心线程数 4 * cores, // 最大线程数 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), // 关键!有界队列 new CustomThreadFactory(), // 命名线程 new LoggingPolicy() // 自定义拒绝策略
);
最佳实践:
-
IO密集型:核心数 = 2 * CPU核数
-
CPU密集型:核心数 = CPU核数 + 1
二、拒绝策略:最后的救命稻草
当队列和线程池全满时,拒绝策略决定了系统生死
1. 四大内置策略对比
策略 | 行为 | 适用场景 |
---|---|---|
AbortPolicy(默认) | 直接抛异常 | 需要快速失败感知 |
CallerRunsPolicy | 提交线程自己执行 | 防止任务丢失但可能阻塞主线程 |
DiscardPolicy | 静默丢弃 | 可容忍数据丢失的监控场景 |
DiscardOldestPolicy | 丢弃队首任务 | 时效性强的场景(如实时报价) |
2. 自定义策略:日志+持久化
class SmartRejectPolicy implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 1. 告警通知 alert("线程池爆炸!当前堆积:"+e.getQueue().size()); // 2. 持久化到Redis redis.save("rejected_tasks", r); // 3. 记录错误日志 log.error("任务被拒绝:"+r.toString()); }
}
真实案例:电商大促时通过该策略挽回超10万笔订单推送
三、队列优化:性能翻倍的关键
1. Tomcat式线程优先策略
class TomcatQueue extends LinkedBlockingQueue<Runnable> { @Override public boolean offer(Runnable task) { // 优先创建线程而非入队 if (executor.getPoolSize() < executor.getMaximumPoolSize()) { return false; // 触发创建新线程 } return super.offer(task); }
}
效果对比:
-
传统策略:先填满队列再创建线程 → 高延迟
-
Tomcat策略:优先创建线程 → 延迟降低40%
2. 延时队列:订单超时关单神器
// 创建延时线程池
ScheduledExecutorService delayPool = Executors.newScheduledThreadPool(2); // 30分钟后执行关单任务
delayPool.schedule(() -> { if(order.isUnpaid()) order.cancel();
}, 30, TimeUnit.MINUTES);
典型场景:
-
订单30分钟未支付自动取消
-
预约提醒提前15分钟推送
-
缓存数据定时刷新
四、实战:推送系统线程池全配置
public class PushThreadPool { // 智能参数配置 private static final int CORE_SIZE = 2 * Runtime.getRuntime().availableProcessors(); private static final int MAX_SIZE = 100; private static final BlockingQueue<Runnable> QUEUE = new TomcatQueue(5000); private static final ExecutorService POOL = new ThreadPoolExecutor( CORE_SIZE, MAX_SIZE, 60, TimeUnit.SECONDS, QUEUE, new NamedThreadFactory("push-worker"), new SmartRejectPolicy() ); // 提交推送任务 public void push(User user, Message msg) { POOL.execute(() -> { // 重试机制(最多3次) for (int i=0; i<3; i++) { if (sendPush(user, msg)) break; } }); }
}
避坑要点:
-
线程命名 → 故障时快速定位
-
有界队列 → 防止内存溢出
-
带重试机制 → 应对网络抖动
五、生产环境监控清单
想要线程池稳定运行,这些监控不能少:
// 实时获取线程池状态
ThreadPoolExecutor pool = (ThreadPoolExecutor) executor; // 核心指标
pool.getActiveCount(); // 活动线程数
pool.getQueue().size(); // 队列堆积数
pool.getCompletedTaskCount(); // 已完成任务量 // 通过JMX动态调优
pool.setCorePoolSize(20); // 流量高峰扩容
pool.setMaximumPoolSize(50);
告警阈值建议:
-
队列堆积 > 80% 容量 → 微信告警
-
活动线程 > 最大线程数90% → 扩容
-
拒绝任务数 > 0 → 立即排查
终极避坑指南
-
线程池不是银弹
-
1000+任务队列?考虑改用消息队列(Kafka/RabbitMQ)
-
长耗时任务?拆分到专用线程池避免阻塞
-
-
参数没有标准答案
// 根据压测结果动态调整 if(isPeakTime()) { pool.setCorePoolSize(50); pool.setMaximumPoolSize(200); }
-
关闭姿势要优雅
pool.shutdown(); // 温柔拒绝新任务 if(!pool.awaitTermination(60, SECONDS)){ pool.shutdownNow(); // 强制终止 }
线程池就像汽车的发动机——参数调得好性能飙升,配错了分分钟爆缸。
记住泪训:永远不用无界队列,始终自定义拒绝策略,关键线程必须命名。