前言
為什麼要用線程池?線程池中的線程可以重複利用,避免了重複建立線程造成的資源開銷。線上程的執行時間比較短,任務比較多的時候非常适合用線程池。
線程池原理及使用
代碼示例
// threadPoolExecutor 最好定義一個全局的,不用每次重建線程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5));
public testThreadPool() {
// 方式1 不需要傳回值
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
// 處理相關業務
}
});
// 方式2 有傳回值
Future<String> submit = threadPoolExecutor.submit(new businessWorker("param"));
}
class businessWorker implements Callable<String> {
public businessWorker(String param) {
}
@Override
public String call() throws Exception {
return null;
}
}
線程池執行步驟
- 如果運作的線程少于核心線程,會嘗試去開啟一個新的核心線程(addWorker),如果有空間的核心線程,也不會去使用,而是去建立一個新的核心線程,直到核心線程建立滿。核心線程建立滿了,線程去隊列中取任務。
- 如果運作的線程數等于核心線程數,任務則進入隊列等待核心線程調用。
- 如果隊列已滿,則去建立非核心線程。
- 如果非核心線程也飽和了,則進入拒絕政策
參數說明
corePoorSize:核心線程數
maximumPoorSize:最大線程數--核心線程數+非核心線程數
keepAliveTime:非核心線程允許空閑時間,超過則非核心線程銷毀
unit:keepAliveTime機關
workQueue:儲存任務的阻塞隊列
threadFactory:建立線程的工廠
hander:線程池飽和的處理方式(拒絕政策)
接下來根據這幾個參數做進一步研究
核心線程數、最大線程數、隊列大小
核心線程數、最大線程數、隊列大小應該設定成多少合适,設定是否合理直接關系到線程池的處理能力。參考網上的說法要根據業務判斷是CPU密集型還是IO密集型
CPU密集型:就會JVM自己内部處理的一些邏輯,額外的IO操作很少
IO密集型:可以了解為經常要和資料庫互動,産生的IO操作比較多
CPU密集型核心線程數:CPU核數(邏輯核) + 1
IO密集型核心線程數:2 * CPU核數(邏輯核)
我們大部分JAVA WEB項目是IO密集型
上面其實就是一個理想狀态的理論值,在實際應用中還會受其它影響,比如一台伺服器不可能隻有這一個應用,這一個應用中也會有其它線程。是以這個核心線程數的設定并不會十分準确,還要線上上使用時來調試這些參數。
首先要通過壓測來設定出一個較為合理的參數。然後簡單做一個線程池的資料監控,根據這些監控資料及線上的運作情況,可以在對線程數做一下适當的調整。線程池中提供了一些資料,可供我們調用
public HashMap<String, Object> getPoolInfo() {
poolInfo.put("activeCount", threadPoolExecutor.getActiveCount());
poolInfo.put("completedTask", threadPoolExecutor.getCompletedTaskCount());
poolInfo.put("poolSize", threadPoolExecutor.getPoolSize());
poolInfo.put("queueSize", threadPoolExecutor.getQueue().size());
poolInfo.put("taskCount", threadPoolExecutor.getTaskCount());
return poolInfo;
}
class AbortPolicyCustom implements RejectedExecutionHandler
{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.info("線程處理異常");
Integer unCompletedTask = Integer.parseInt(String.valueOf(poolInfo.get("unCompletedTask") == null ? 0 : poolInfo.get("unCompletedTask")));
poolInfo.put("unCompletedTask", unCompletedTask + 1);
}
}
{
"activeCount":0,
"taskCount":1000,
"queueSize":0,
"poolSize":100,
"completedTask":1000,
"unCompletedTask": 0
}
taskCount: 請求過來的總任務數
poolSize:目前線程池的線程數
completeTaskCount:已完成的任務數
activeCount:目前正在運作的線程數
getQueue().size():可以擷取目前隊列中的任務數
unCompletedTask: 未完成任務數(需要通過拒絕政策自定義實作)
經驗總結:
1. 如果業務經常用
- 要求響應時間越快越好,可以适當将核心線程數和最大線程數調大一點,隊列不用設定太大。這樣既能提高響應,也不會浪費線程資源。
- 不要求響應時間,隊列可以設定大一點。
2. 如果業務不常使用
核心線程數可以設定小一點,最大線程數可以調成2倍核心線程數,這樣如果核心線程不夠用的話,可以建立非核心線程,待任務執行完後,到達空閑等待時間後銷毀非核心線程,隻保留了少數核心線程數,這樣也不會浪費線程資源。
3. 批量處理大量任務
不宜将核心線程數設定的太大,設定的太大可能會引起線程的上下文切換帶來的問題,也降低了處理速度,可以适當将隊列設定大一點,不要求響應時間,慢慢處理就好了,注意做好拒絕政策處理,避免任務丢失。
拒絕政策
總共有4種拒絕政策
- AbortPolicy:抛出異常(預設)
- CallerRunsPolicy:送出任務的線程自己執行
- DisCardOldestPolicy:把老的任務丢掉
- DiscardPolicy:什麼都沒幹,直接丢掉任務
經驗來說,大多數情況使用預設的抛出異常即可,這樣保證即使任務丢失後,也能做好後續處理。
public void testThreadPool() {
long l = System.currentTimeMillis();
try {
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
handleMsg(l);
}
});
} catch (Exception e) {
log.info("任務處理異常:{}", e);
Integer unCompletedTask = Integer.parseInt(String.valueOf(poolInfo.get("unCompletedTask") == null ? 0 : poolInfo.get("unCompletedTask")));
poolInfo.put("unCompletedTask", unCompletedTask + 1);
}
}
如果第二個政策,讓主線程自己執行,任務可以不丢失,但是如果出現大量任務被拒絕的話,主線程性能會直線下降。