為什么線程池總在深夜崩潰?
昨天我這項目又經歷了一次爆破——路由推送服務突然崩潰,排查發現線程池隊列堆積了幾萬任務直接把內存撐爆。早上起來看見人都麻了,線程池用不好,分分鐘變系統炸彈。今天我們就來系統梳理線程池的實戰技巧。
一、四大線程池類型:用錯場景就是災難
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(); // 強制終止 }
線程池就像汽車的發動機——參數調得好性能飆升,配錯了分分鐘爆缸。
記住淚訓:永遠不用無界隊列,始終自定義拒絕策略,關鍵線程必須命名。