線程池
線程是寶貴的內(nèi)存資源枢希、單個(gè)線程約占1MB空間,過(guò)多分配易造成內(nèi)存溢出
頻繁的創(chuàng)建及銷(xiāo)毀線程會(huì)增加虛擬機(jī)回收頻率劳淆、資源開(kāi)銷(xiāo)兰伤,造成程序性能下降
線程池是線程的容器∧砑ぃ可設(shè)定線程分配的數(shù)量上限噪矛,將預(yù)先創(chuàng)建的線程對(duì)象存入池中,并重用線程池中的線程對(duì)象铺罢,避免線程頻繁的創(chuàng)建和銷(xiāo)毀艇挨。
一、JDK提供創(chuàng)建線程池的四種快捷方式
//創(chuàng)建一個(gè)單線程化的線程池韭赘,它只會(huì)用唯一的工作線程來(lái)執(zhí)行任務(wù)缩滨,保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行。
//適用于需要保證順序執(zhí)行各個(gè)任務(wù)泉瞻。
ExecutorService pool01 = Executors.newSingleThreadExecutor();
//創(chuàng)建一個(gè)定長(zhǎng)線程池脉漏,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待袖牙。因?yàn)椴捎脽o(wú)界的阻塞隊(duì)列侧巨,所以實(shí)際線程數(shù)量永遠(yuǎn)不會(huì)變化。
//適用于負(fù)載較重的場(chǎng)景鞭达,對(duì)當(dāng)前線程數(shù)量進(jìn)行限制司忱。(保證線程數(shù)可控皇忿,不會(huì)造成線程過(guò)多,導(dǎo)致系統(tǒng)負(fù)載更為嚴(yán)重)
ExecutorService pool02 = Executors.newFixedThreadPool(4);
//創(chuàng)建一個(gè)可緩存線程池坦仍,如果線程池長(zhǎng)度超過(guò)處理需要鳍烁,可靈活回收空閑線程,若無(wú)可回收繁扎,則新建線程幔荒。
//適用于負(fù)載較輕的場(chǎng)景,執(zhí)行短期異步任務(wù)梳玫。(可以使得任務(wù)快速得到執(zhí)行爹梁,因?yàn)槿蝿?wù)時(shí)間執(zhí)行短,可以很快結(jié)束提澎,也不會(huì)造成cpu過(guò)度切換)
ExecutorService pool03 = Executors.newCachedThreadPool();
//創(chuàng)建一個(gè)定長(zhǎng)線程池姚垃,支持定時(shí)及周期性任務(wù)執(zhí)行。
//適用于執(zhí)行延時(shí)或者周期性任務(wù)虱朵。
ScheduledExecutorService pool04 = Executors.newScheduledThreadPool(4);
二莉炉、工作原理
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
int corePoolSize:核心線程數(shù)
核心線程會(huì)一直存活钓账,即使沒(méi)有任務(wù)需要執(zhí)行碴犬。
當(dāng)線程數(shù)小于核心線程數(shù)時(shí),即使有線程空閑梆暮,線程池也會(huì)優(yōu)先創(chuàng)建新線程處理服协。
設(shè)置 allowCoreThreadTimeout=true(默認(rèn)false)時(shí),核心線程會(huì)超時(shí)關(guān)閉啦粹。
可以通過(guò) prestartCoreThread() 或 prestartAllCoreThreads() 方法來(lái)提前啟動(dòng)線程池中的基本線程偿荷。
int maxPoolSize:最大線程數(shù)
線程池所允許的最大線程個(gè)數(shù)
maxPoolSize>當(dāng)線程數(shù)>=corePoolSize,且任務(wù)隊(duì)列已滿時(shí)唠椭。線程池會(huì)創(chuàng)建新線程來(lái)處理任務(wù)跳纳。
當(dāng)線程數(shù)=maxPoolSize,且任務(wù)隊(duì)列已滿時(shí)贪嫂,線程池會(huì)根據(jù)handle策略處理寺庄,默認(rèn)是AbortPolicy 丟棄任務(wù),拋運(yùn)行時(shí)異常力崇。
long keepAliveTime:非核心線程空閑保持時(shí)間
當(dāng)線程空閑時(shí)間達(dá)到 keepAliveTime 時(shí)斗塘,線程會(huì)退出,直到線程數(shù)量= corePoolSize亮靴。
如果 allowCoreThreadTimeout = true馍盟,則會(huì)直到線程數(shù)量=0。
TimeUnit unit:時(shí)間單位
TimeUnit是一個(gè)枚舉類(lèi)型 茧吊,包括以下屬性:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小時(shí) DAYS : 天
BlockingQueue workQueue:任務(wù)隊(duì)列容量(阻塞隊(duì)列)
當(dāng)核心線程數(shù)達(dá)到最大時(shí)贞岭,新任務(wù)會(huì)放在隊(duì)列中排隊(duì)等待執(zhí)行八毯。
-
常用的幾個(gè)阻塞隊(duì)列:
-
LinkedBlockingQueue
鏈?zhǔn)阶枞?duì)列,底層數(shù)據(jù)結(jié)構(gòu)是鏈表曹步,默認(rèn)大小是
Integer.MAX_VALUE
宪彩,也可以指定大小。 -
ArrayBlockingQueue
數(shù)組阻塞隊(duì)列讲婚,底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組尿孔,需要指定隊(duì)列的大小。
-
SynchronousQueue
同步隊(duì)列筹麸,內(nèi)部容量為0活合,每個(gè)put操作必須等待一個(gè)take操作,反之亦然物赶。
-
DelayQueue
延遲隊(duì)列白指,該隊(duì)列中的元素只有當(dāng)其指定的延遲時(shí)間到了,才能夠從隊(duì)列中獲取到該元素 酵紫。
-
ThreadFactory threadFactory:線程工廠
用于批量創(chuàng)建線程告嘲,統(tǒng)一在創(chuàng)建線程時(shí)設(shè)置一些參數(shù),如是否守護(hù)線程奖地、線程的優(yōu)先級(jí)等橄唬。如果不指定,會(huì)新建一個(gè)默認(rèn)的線程工廠参歹。
RejectedExecutionHandler handler:任務(wù)拒絕處理器
-
兩種情況會(huì)拒絕處理任務(wù):
- 當(dāng)線程數(shù)已經(jīng)達(dá)到 maxPoolSize仰楚,切隊(duì)列已滿,會(huì)拒絕新任務(wù)犬庇。
- 當(dāng)線程池被調(diào)用 shutdown() 后僧界,會(huì)等待線程池里的任務(wù)執(zhí)行完畢,再 shutdown臭挽。如果在調(diào)用shutdown() 和線程池真正 shutdown 之間提交任務(wù)捂襟,會(huì)拒絕新任務(wù)。線程池會(huì)調(diào)用rejectedExecutionHandler 來(lái)處理這個(gè)任務(wù)欢峰。如果沒(méi)有設(shè)置默認(rèn)是 AbortPolicy葬荷,會(huì)拋出異常。
-
ThreadPoolExecutor 類(lèi)有幾個(gè)內(nèi)部實(shí)現(xiàn)類(lèi)來(lái)處理這類(lèi)情況-handle飽和策略:
- ThreadPoolExecutor.AbortPolicy:默認(rèn)拒絕處理策略赤赊,丟棄任務(wù)并拋出RejectedExecutionException異常闯狱。
- ThreadPoolExecutor.DiscardPolicy:丟棄新來(lái)的任務(wù),但是不拋出異常抛计。
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊(duì)列頭部(最舊的)的任務(wù)哄孤,然后重新嘗試執(zhí)行程序(如果再次失敗,重復(fù)此過(guò)程)吹截。
- ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線程處理該任務(wù)瘦陈。
三凝危、線程池使用不當(dāng)?shù)奈:?/h2>
不知道大家在使用線程池的時(shí)候,是否存在以下的疑惑晨逝,這是我看到的某些同事的代碼蛾默,他們是這樣使用線程池的:
1、 線程池局部變量捉貌,new出來(lái)使用支鸡,沒(méi)有手動(dòng)shutdown
2、 線程池局部變量趁窃,new出來(lái)使用牧挣,并且最后手動(dòng)shutdown
3、 線程池定義為static類(lèi)型醒陆,進(jìn)行類(lèi)復(fù)用
大家先想想到底哪種方式是正確的瀑构,以及錯(cuò)誤的方式可能會(huì)帶來(lái)什么問(wèn)題
方式一:線程池局部使用,沒(méi)有shutdown
首先刨摩,我們明確:局部變量new出來(lái)的線程池寺晌,執(zhí)行這段代碼的程序的每一個(gè)線程都會(huì)去創(chuàng)建一個(gè)局部的線程池。暫且不說(shuō)每一個(gè)線程都去創(chuàng)建線程池是出于什么神奇的目的澡刹,首當(dāng)其沖的線程池的復(fù)用的性質(zhì)就被打破了呻征。創(chuàng)建出來(lái)的線程池都得不到復(fù)用,那么還有什么必要花費(fèi)大精力創(chuàng)建線程池像屋?
所以線程池局部使用本身就是不推薦的使用方式怕犁!
其次边篮,我們?cè)賮?lái)想想己莺,局部使用線程池,同時(shí)設(shè)置核心線程不為0戈轿,且設(shè)置allowCoreThreadTimeOut=false(空閑后不回收核心線程池)會(huì)導(dǎo)致什么問(wèn)題凌受?(想都不用想,核心線程池得不到回收思杯,自然會(huì)導(dǎo)致OOM)
以下是問(wèn)題代碼:
public static void main(String[] args) {
while (true) {
try {
//newFixedThreadPool不會(huì)回收核心線程 可能導(dǎo)致OOM
ExecutorService service = Executors.newFixedThreadPool(1);
service.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000); 模擬處理業(yè)務(wù)
} catch (InterruptedException e) {
}
}
});
service = null;
} catch (Exception e) {
}
try {
Thread.sleep(2000);
System.gc();
} catch (InterruptedException e) {
}
}
}
那么胜蛉,是否我們?cè)O(shè)置核心線程可以回收不就好咯?同樣會(huì)出現(xiàn)問(wèn)題色乾。系統(tǒng)可能會(huì)根據(jù)你設(shè)置的線程過(guò)期時(shí)間誊册,呈現(xiàn)有規(guī)律的內(nèi)存占用上升,然后下降暖璧,然后又上升案怯,然后又下降的趨勢(shì)。你說(shuō)說(shuō)這是好的內(nèi)存運(yùn)行情況澎办?
方式二:線程池局部使用嘲碱,使用完后手動(dòng)shutdown線程池
okok金砍,這種方式OOM的風(fēng)險(xiǎn)降低了,但是又是局部使用局部使用麦锯,你干嘛要局部使用線程池呢恕稠?這樣不就使得每一個(gè)線程都會(huì)new一個(gè)線程池,導(dǎo)致線程池不會(huì)復(fù)用扶欣,這和你不用線程池有什么區(qū)別呢鹅巍?系統(tǒng)還白白花費(fèi)資源去創(chuàng)建線程池。
方式三:線程池定義為static類(lèi)型料祠,進(jìn)行類(lèi)復(fù)用
明顯昆著,到這里才是正確的使用線程池的方式。static修飾的類(lèi)變量只會(huì)加載一次术陶,所有的線程共享這一個(gè)線程池了唄凑懂。以下是正確的使用代碼:
public class staticTestExample {
//static 定義線程池
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
timeout,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
//使用線程池
Future<Boolean> submit = pool.submit(() -> true);
}
}
業(yè)務(wù)中的線程池的復(fù)用
這里給出以下兩種思路
第一種:根據(jù)業(yè)務(wù)執(zhí)行類(lèi)型去創(chuàng)建線程池(同一類(lèi)型的業(yè)務(wù)復(fù)用一個(gè)線程池)
??簡(jiǎn)單來(lái)說(shuō),業(yè)務(wù)場(chǎng)景相同梧宫,且需要用到線程池的地方接谨,復(fù)用一個(gè)線程池。比如塘匣,拆分任務(wù)場(chǎng)景脓豪,一次性需要同時(shí)拆分100個(gè)任務(wù)去執(zhí)行,就可以把這100個(gè)相同業(yè)務(wù)場(chǎng)景的任務(wù)交給一個(gè)特定命名的線程池處理忌卤。這個(gè)線程池就是專(zhuān)門(mén)去處理任務(wù)拆分的扫夜。
代碼如下:
public class ThreadPoolUtil {
private static final Map<String, ExecutorService> POOL_CATCH = Maps.newConcurrentMap();
/**
* 根據(jù)特定名稱(chēng)去緩存線程池
* @param poolName
* @return
*/
public static ExecutorService create(String poolName) {
//map有緩存直接復(fù)用緩存中存在的線程池
if (POOL_CATCH.containsKey(poolName)) {
return POOL_CATCH.get(poolName);
}
// synchronized鎖字符串常量池字段 防止并發(fā),使得map中緩存的線程池驰徊,只會(huì)創(chuàng)建一次
synchronized (poolName.intern()) {
int poolSize = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(poolSize, poolSize,
30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),new ThreadPoolExecutor.AbortPolicy());
POOL_CATCH.put(poolName, threadPoolExecutor);
return threadPoolExecutor;
}
}
}
public class CorrectTest {
public static void main(String[] args) {
ExecutorService pool = ThreadPoolUtil.create("拆分任務(wù)線程池");
//線程池執(zhí)行任務(wù)
Future<Boolean> submit = pool.submit(() -> true);
}
}
第二種:根據(jù)用戶登陸性質(zhì)去創(chuàng)建線程池(用一類(lèi)型的用戶復(fù)用一個(gè)線程池)
??簡(jiǎn)單來(lái)說(shuō)笤闯,用戶類(lèi)型且業(yè)務(wù)場(chǎng)景相同,需要用到線程池的地方棍厂,復(fù)用一個(gè)線程池颗味。
public class CorrectTest {
public static void main(String[] args) {
//模擬得到用戶信息
Userinfo = getUserinfo();
//模擬用相同的用戶類(lèi)型(type)去創(chuàng)建線程池
ExecutorService pool = ThreadPoolUtil.create(Userinfo.getType);
//線程池執(zhí)行任務(wù)
Future<Boolean> submit = pool.submit(() -> true);
}
}
總結(jié)
- 使用全局線程池而不是局部線程池,否則可能會(huì)有連續(xù)創(chuàng)建局部線程池的OOM風(fēng)險(xiǎn)
- 就算使用局部線程池牺弹,最后一定要shutdown浦马,否則可能導(dǎo)致不回收核心線程的內(nèi)存泄漏
- 理解線程池是為了復(fù)用的,不要代碼中隨意new一個(gè)局部線程池
四张漂、結(jié)語(yǔ)
四種常見(jiàn)的線程池基本夠我們使用了晶默,但是《阿里巴巴開(kāi)發(fā)手冊(cè)》不建議我們直接使用Executors類(lèi)中的線程池,而是通過(guò)ThreadPoolExecutor
的方式航攒,這樣的處理方式讓寫(xiě)的同學(xué)需要更加明確線程池的運(yùn)行規(guī)則磺陡,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
但如果你及團(tuán)隊(duì)本身對(duì)線程池非常熟悉,又確定業(yè)務(wù)規(guī)模不會(huì)大到資源耗盡的程度(比如線程數(shù)量或任務(wù)隊(duì)列長(zhǎng)度可能達(dá)到Integer.MAX_VALUE)時(shí)仅政,其實(shí)是可以使用JDK提供的這幾個(gè)接口的垢油,它能讓我們的代碼具有更強(qiáng)的可讀性。