系統(tǒng)性能優(yōu)化的幾種常用手段是異步和緩存濒旦。因此我們常常使用線程池異步處理一些業(yè)務株旷。
線程池的使用還是相對比較簡單的,首先創(chuàng)建一個線程池尔邓,然后通過execute或submit執(zhí)行任務晾剖。
但魔鬼往往藏于細節(jié)之中,稍有不慎就會出錯梯嗽。本文將會詳細總結(jié)線程池容易出錯的五大坑
一齿尽、拒絕策略參數(shù)知多少
二、拒絕策略使用不當灯节,系統(tǒng)阻塞不可用
三雕什、多任務get()異常時,結(jié)果獲取有誤
四、ThreadLocal與線程池搭配使用显晶,上下文缺失
五贷岸、父子任務共用同一線程池,系統(tǒng)“饑餓”死鎖
以下為線程池的核心流程【具體內(nèi)容參考:線程池原理】
一磷雇、拒絕策略參數(shù)知多少
我們都知道偿警,當任務過多,線程池處理不過來時會被拒絕唯笙,進入拒絕策略
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
通過實現(xiàn)RejectedExecutionHandler螟蒸,就可以作為線程池的拒絕策略使用盒使。
目前官方提供了四種拒絕策略,分別為:
- CallerRunsPolicy:由任務調(diào)用方執(zhí)行
- AbortPolicy:拋出異常七嫌,同樣也是由任務調(diào)用方處理異常
- DiscardPolicy:丟棄當前任務
- DiscardOldestPolicy:丟棄隊列中最老的任務少办,并執(zhí)行當前任務
線程池有execute和submit兩種方法執(zhí)行任務:
execute執(zhí)行我們最原始的任務;
而submit則不同诵原,先是將我們最原始的任務封裝成FutureTask任務英妓,然后將FutureTask任務交由execute執(zhí)行
線程池拒絕策略中Runnable r就是execute執(zhí)行的任務,因此當使用r時就要注意它是我們最原始的任務還是FutureTask任務
二绍赛、拒絕策略使用不當蔓纠,系統(tǒng)阻塞不可用
前面我們講到submit方法執(zhí)行任務時,線程池會先封裝任務到FutureTask中吗蚌,然后我們通過FutureTask的get()方法獲取任務處理的結(jié)果
【具體內(nèi)容參考:一張動圖腿倚,徹底懂了execute和submit】
Possible state transitions:
NEW -> COMPLETING -> NORMAL(任務執(zhí)行完成)
NEW ->COMPLETING -> EXCEPTIONAL(任務拋出異常)
NEW -> CANCELLED(任務被取消)
NEW -> INTERRUPTING -> INTERRUPTED(任務被打斷)
FutureTask在被創(chuàng)建時狀態(tài)為NEW,任務執(zhí)行到某個階段就會修改成相應狀態(tài)蚯妇,直到達到最終態(tài)敷燎。
FutureTask根據(jù)狀態(tài)變更來標識任務執(zhí)行進度的,因此get()方法也是在狀態(tài)達到最終態(tài)(任務執(zhí)行成果/異常/被取消/被打斷)時才能返回結(jié)果箩言,否則掛起當前線程等待到達最終態(tài)硬贯。
問題原因:
1、當任務通過submit方法執(zhí)行時分扎,會創(chuàng)建FutureTask(此時狀態(tài)為NEW)
2澄成、任務被拒絕且拒絕策略為丟棄任務(DiscardOleddestPolicy或DiscardPolicy)時胧洒,任務直接被線程池丟棄(此時狀態(tài)仍為NEW)
3畏吓、當執(zhí)行g(shù)et()方法時,由于任務一直處于NEW狀態(tài)卫漫,沒有達到最終態(tài)菲饼,線程會一直處于阻塞狀態(tài)
解決方案:
問題原因在于:任務無法變成最終態(tài),導致阻塞列赎。
因此我們可以重寫rejectedExecution方法,將任務置為最終態(tài)
FutureTask的cancel方法可以將任務狀態(tài)置為CANCELLED或INTERRUPTED
public static RejectedExecutionHandler customDiscardPolicy () {
return new DiscardPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
if (r != null && r instanceof FutureTask) {
((FutureTask) r).cancel(true);
}
}
}
};
}
三、多任務get()異常時,結(jié)果獲取有誤
submit方法中黍聂,futureTask會捕獲異常腹纳,在get()時拋出。
若批量執(zhí)行多個方法诗越,且for循環(huán)get()結(jié)果時砖瞧,捕獲異常要在循環(huán)內(nèi),而不是循環(huán)外嚷狞。否則會影響其他任務的結(jié)果輸出
捕獲異常在循環(huán)外块促,當一個任務get異常時荣堰,后續(xù)其他任務就不能再獲取結(jié)果
List<TaskResult> taskResultList = new ArrayList<>();
try {
for (Future<TaskResult> future : futureList) {
if (future == null) {continue;}
TaskResult result = future.get();
taskResultList.add(result);
}
} catch (Throwable t) {
//這種場景下,當一個任務get異常時竭翠,后續(xù)其他任務就不能再獲取結(jié)果
LOGGER.error("任務執(zhí)行異常", t);
}
因此在循環(huán)內(nèi)捕獲異常振坚,各個任務互相不受影響
List<TaskResult> taskResultList = new ArrayList<>();
for (Future<TaskResult> future : futureList) {
try {
if (future == null) {continue;}
TaskResult result = future.get();
taskResultList.add(result);
} catch (Throwable t) {
LOGGER.error("任務執(zhí)行異常", t);
}
}
四、ThreadLocal與線程池搭配使用斋扰,上下文缺失
ThreadLocal的使用一般都是這幾個方法:
private final static ThreadLocal<CacheInfo> cacheInfoThreadLocal = new ThreadLocal<CacheInfo>();
cacheInfoThreadLocal.set(cacheInfo);
cacheInfoThreadLocal.get();
cacheInfoThreadLocal.remove();
為防止內(nèi)存泄漏渡八,在使用完ThreadLocal后都會調(diào)用remove()清除數(shù)據(jù)
問題描述:
1、當任務需要調(diào)用方線程的ThreadLocal信息時褥实,通用方式就是將調(diào)用方ThreadLocal信息賦值到執(zhí)行任務的線程中呀狼,在任務執(zhí)行結(jié)束后調(diào)用remove()清除數(shù)據(jù)
2、同時任務恰好被線程池拒絕损离,且使用的拒絕策略是CallerRunsPolicy時哥艇,任務會被調(diào)用方線程執(zhí)行。
3僻澎、若此時任務執(zhí)行結(jié)束后仍調(diào)用remove()清除數(shù)據(jù)貌踏,清除的就會是調(diào)用方的ThreadLocal數(shù)據(jù)。
調(diào)用方ThreadLocal數(shù)據(jù)被清除窟勃,數(shù)據(jù)丟失在工作中將會是災難性的祖乳。
解決方案:
問題出現(xiàn)的原因是任務由于被拒絕,導致誤刪除了調(diào)用方ThreadLocal數(shù)據(jù)
因此可以在任務執(zhí)行時判斷執(zhí)行線程是否為調(diào)用方線程秉氧。
若是則不用set()復制和remove()清空數(shù)據(jù)
public abstract class ParallelCallableTask<V> implements Callable<V> {
//調(diào)用方線程名稱
private String mainThreadName;
public ParallelCallableTask() {
mainThreadName = Thread.currentThread().getName();
}
@Override
public V call() throws Exception {
//是否為同一線程
boolean sameThread = sameThread();
return proccess(sameThread);
}
/**判斷 調(diào)用方線程 和 執(zhí)行線程 是否為同一線程*/
private boolean sameThread () {
String curThreadName = Thread.currentThread().getName();
return curThreadName.equals(mainThreadName);
}
//任務重寫這個方法并根據(jù)sameThread判斷是否需要set和remove調(diào)用方線程的ThreadLocal數(shù)據(jù)
public abstract V proccess(boolean sameThread);
}
待執(zhí)行的任務通過重寫process方法眷昆,并根據(jù)sameThread判斷是否和主線程一致,一致則不重復設置相同的threadLocal和刪除threadLocal
五汁咏、父子任務共用同一線程池亚斋,系統(tǒng)“饑餓”死鎖
A方法調(diào)用B方法,AB方法稱為父子任務攘滩。
當他們都被同一個線程池執(zhí)行時帅刊,一定條件下會出現(xiàn)以下場景:
1、父任務獲取到線程池線程執(zhí)行漂问,而子任務則被暫存到隊列中
2赖瞒、當父任務占滿了線程池所有的線程,等待子任務返回結(jié)果后蚤假,結(jié)束父任務
3栏饮、此時子任務由于在隊列中,一直不能等到線程來處理磷仰,導致不能從隊列中釋放
4袍嬉、父子任務互相等待,從而造成“饑餓”死鎖
我們舉一個簡單例子:
假設線程池參數(shù)設置為:核心和最大線程數(shù)為1芒划,隊列容量為1
A方法內(nèi)調(diào)用B方法:
A() {
B()冬竟;
}
現(xiàn)在父子任務都被同一個線程池進行調(diào)用欧穴,整個流程為(如圖所示):
1、線程池創(chuàng)建核心線程泵殴,并執(zhí)行A方法
2涮帘、執(zhí)行到B方法時,將B交給線程池執(zhí)行笑诅,由于沒有多余線程调缨,因此暫存隊列
3、A任務等待B任務執(zhí)行完吆你,B任務等待A任務釋放線程弦叶。從而互相等待,造成“饑餓”死鎖
解決方案:
問題原因在于互相等待妇多,因此只要保證類似的父子任務不要被同一線程池執(zhí)行即可
------The End------
如果這個辦法對您有用伤哺,或者您希望持續(xù)關(guān)注,可以在wx公眾號中搜索【碼路無涯】者祖,期待你的到來