上個月服務上線後,用户反饋接口很慢,平均響應時間2秒多。
排查了一圈,發現是線程池配置不當導致的。
調優之後,響應時間降到200ms,記錄一下完整過程。
問題現象
用户反饋下單接口很慢,看了下監控:
- 平均響應時間:2.3秒
- P99響應時間:5秒+
- 偶爾還會超時
但CPU、內存、數據庫都正常,沒有明顯瓶頸。
排查過程
第一步:看線程池狀態
用Arthas看了下線程池:
# 進入Arthas
java -jar arthas-boot.jar
# 查看線程池狀態
thread -n 3
發現大量線程處於WAITING狀態,在等待任務。
再看線程池的具體參數:
// 項目中的配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000) // 隊列容量
);
問題找到了:核心線程數只有10,但隊列容量是10000。
第二步:分析問題
這個配置的問題:
- 核心線程數太小:只有10個線程處理任務
- 隊列太大:新任務會先進隊列,而不是創建新線程
- 最大線程數等於核心線程數:隊列滿了才會創建新線程,但隊列有10000容量,幾乎不會滿
結果:高併發時,任務在隊列裏排隊等待,響應時間自然就慢了。
線程池工作原理
先複習一下線程池的工作流程:
新任務到來
↓
當前線程數 < corePoolSize?
↓ 是
創建新線程執行
↓ 否
隊列未滿?
↓ 是
放入隊列等待
↓ 否
當前線程數 < maximumPoolSize?
↓ 是
創建新線程執行
↓ 否
執行拒絕策略
關鍵點:任務會優先進隊列,而不是創建新線程!
這就是為什麼隊列太大會導致響應慢——任務都在排隊。
優化方案
方案1:調整參數
// 優化後的配置
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
cpuCores * 2, // corePoolSize:CPU核心數的2倍
cpuCores * 4, // maximumPoolSize:CPU核心數的4倍
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 隊列容量減小
new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略
);
參數計算公式:
- CPU密集型:
corePoolSize = CPU核心數 + 1 - IO密集型:
corePoolSize = CPU核心數 * 2(或更高)
我們的業務是IO密集型(有數據庫查詢、RPC調用),所以用CPU核心數 * 2。
方案2:使用SynchronousQueue
如果想讓任務儘快被執行,可以用SynchronousQueue:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10,
100, // 最大線程數調大
60L,
TimeUnit.SECONDS,
new SynchronousQueue<>(), // 不緩存任務
new ThreadPoolExecutor.CallerRunsPolicy()
);
SynchronousQueue不存儲任務,新任務來了直接創建線程執行。
方案3:動態線程池(推薦)
更好的方案是動態調整線程池參數:
@Component
public class DynamicThreadPool {
private ThreadPoolExecutor executor;
@PostConstruct
public void init() {
executor = new ThreadPoolExecutor(
20, 50, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// 動態調整核心線程數
public void setCorePoolSize(int size) {
executor.setCorePoolSize(size);
}
// 動態調整最大線程數
public void setMaxPoolSize(int size) {
executor.setMaximumPoolSize(size);
}
// 獲取線程池狀態
public Map<String, Object> getStatus() {
Map<String, Object> status = new HashMap<>();
status.put("corePoolSize", executor.getCorePoolSize());
status.put("maximumPoolSize", executor.getMaximumPoolSize());
status.put("activeCount", executor.getActiveCount());
status.put("queueSize", executor.getQueue().size());
status.put("completedTaskCount", executor.getCompletedTaskCount());
return status;
}
}
配合配置中心(Nacos/Apollo),可以在線調整參數,不用重啓服務。
優化後效果
調整參數後,對比數據:
| 指標 | 優化前 | 優化後 |
|---|---|---|
| 核心線程數 | 10 | 32 |
| 最大線程數 | 10 | 64 |
| 隊列容量 | 10000 | 200 |
| 平均響應時間 | 2.3秒 | 180ms |
| P99響應時間 | 5秒+ | 500ms |
效果:響應時間降低了10倍以上。
監控告警
優化完不能不管了,要加監控:
@Scheduled(fixedRate = 60000)
public void monitorThreadPool() {
int activeCount = executor.getActiveCount();
int queueSize = executor.getQueue().size();
int poolSize = executor.getPoolSize();
// 記錄到監控系統
log.info("ThreadPool status: active={}, queue={}, pool={}",
activeCount, queueSize, poolSize);
// 隊列積壓告警
if (queueSize > 100) {
alertService.send("線程池隊列積壓: " + queueSize);
}
// 線程數告警
if (activeCount >= executor.getMaximumPoolSize() * 0.8) {
alertService.send("線程池接近飽和: " + activeCount);
}
}
常見錯誤配置
錯誤1:隊列無界
new LinkedBlockingQueue<>() // 默認是Integer.MAX_VALUE
問題:任務無限堆積,最終OOM。
錯誤2:核心線程數太小
corePoolSize = 5 // 8核CPU只配5個核心線程
問題:CPU利用率低,任務排隊等待。
錯誤3:拒絕策略選錯
new ThreadPoolExecutor.AbortPolicy() // 直接拋異常
問題:高併發時大量任務被拒絕,用户看到報錯。
建議:用CallerRunsPolicy,讓調用線程自己執行,起到限流作用。
線程池配置建議
| 場景 | corePoolSize | maximumPoolSize | 隊列 |
|---|---|---|---|
| CPU密集型 | N+1 | N+1 | 小隊列(100以內) |
| IO密集型 | 2N | 4N | 中等隊列(200-500) |
| 混合型 | N*1.5 | 2N | 根據實際調整 |
(N = CPU核心數)
遠程排查技巧
如果線上服務出問題,需要遠程查看線程池狀態,可以:
- Arthas:
thread命令看線程狀態 - JMX:通過JMX遠程連接查看
- 自定義接口:暴露線程池狀態接口
如果服務器在內網,可以用星空組網工具把本地和服務器連起來,直接用IDE的Remote Debug功能,比看日誌效率高很多。
總結
| 優化點 | 説明 |
|---|---|
| 增大核心線程數 | IO密集型用 CPU核心數*2 |
| 減小隊列容量 | 避免任務積壓 |
| 合理設置最大線程數 | 給突發流量留餘地 |
| 選對拒絕策略 | CallerRunsPolicy比較穩 |
| 加監控告警 | 及時發現問題 |
核心原則:讓任務儘快被線程執行,而不是在隊列裏排隊。
線程池配置踩過其他坑的,歡迎評論區交流~