相信大家都使用過線程池娜睛,也了解使用線程池的好處。我們使用線程池最多的還是使用Executors工具類創(chuàng)建FixedThreadPool率翅、SingleThreadPool以及CachedThreadPool三種線程池,如果我們不了解其工作原理,將會碰到很多意想不到的問題念秧,例如內(nèi)存被撐爆,cpu被打滿布疼,線程池?zé)o故中斷摊趾,關(guān)閉線程池應(yīng)該使用shutdown()還是shutdownNow()等等一系列的問題,這篇文章將講解為什么要是用線程池缎除,為什么會出現(xiàn)上述的問題严就,線程池的工作原理是什么,應(yīng)該選擇何種線程池器罐,如何定義線程池線程的個數(shù)等等梢为。本文采用JDK8源碼進行講解,主要講解原理為主轰坊,不過多的涉及到源碼铸董。
注:手機端代碼展示不佳,建議查看原文鏈接
線程是什么
不講書本知識肴沫,只是拋出一個問題:new Thread(); new Runnable(); new Callable(); (盡管不能new 接口粟害,這里只是說明意思,不要較真)颤芬,請問這代表線程嗎悲幅?
一定要注意,這都是類站蝠,java中的類汰具,不是線程,千萬不要看到thread菱魔, runnable留荔, callable就認(rèn)為是線程,它們和Object澜倦, List聚蝶,Map一樣是java語言的類而已杰妓。 或者可以說他們是任務(wù),線程執(zhí)行的任務(wù)碘勉,因為他們內(nèi)部都有線程要執(zhí)行的方法run()或者call()巷挥,那什么才是線程呢?
new Thread().start(); 調(diào)用了start()方法才會在操作系統(tǒng)層面啟動一個線程恰聘,除此之外都是承載了線程執(zhí)行方法的類而已句各。這一點大家一定要分清楚。
多線程
多線程一定比單線程快嗎晴叨?這個答案是否定的凿宾,在單核處理器環(huán)境下,多個線程執(zhí)行任務(wù)勢必會引起線程上下文切換兼蕊,上下文切換會對當(dāng)前線程的執(zhí)行環(huán)境進行保存初厚,并還原將要執(zhí)行線程的執(zhí)行環(huán)境,存在開銷孙技。多線程與單線程相比产禾,多出了上下文切換的時間,因此在單核處理器環(huán)境下牵啦,多線程并不會提高性能亚情。
現(xiàn)今,處理基本上都是多核多處理器哈雏,因此合理使用多線程編程將取很大的性能提升楞件。但是當(dāng)線程數(shù)過多,引起過多的上下文切換裳瘪,當(dāng)上下文切換的開銷大于多線程帶來的收益的話土浸,性能將會下降。濫用多線程將會是一場災(zāi)難彭羹。
線程池的引入
首先從線程類Thread講起黄伊,Thread類具有兩個功能:
- 維護線程 線程的創(chuàng)建、休眠派殷、中斷还最、暫停、銷毀
- 執(zhí)行任務(wù) Thread類及其子類run()毡惜, Runnable對象的run(), Callable對象的call() (更準(zhǔn)確的說應(yīng)該是FutureTask憋活,因為Callable對象并不能傳入Thread類)
Thread類將線程和任務(wù)耦合在一起,一般的使用方式為:有多少個任務(wù)就需要多少個線程去執(zhí)行虱黄,并發(fā)的任務(wù)數(shù)太多,就會引起大量的上下文切換吮成,以及線程的創(chuàng)建與銷毀(線程的創(chuàng)建和銷毀都設(shè)計到內(nèi)核態(tài)和用戶態(tài)的轉(zhuǎn)換橱乱,開銷也不容小覷)辜梳。
為了能對線程進行統(tǒng)一的管理和復(fù)用,引入了線程池泳叠。線程池對線程進行統(tǒng)一的管理作瞄,并可以彈性的擴展,將執(zhí)行任務(wù)和線程完全分離危纫,任務(wù)存放到阻塞隊列中宗挥,線程不斷的去阻塞隊列中取任務(wù)執(zhí)行。從而達到線程復(fù)用的目的(說白了种蝶,線程在死循環(huán)中去阻塞隊列獲取數(shù)據(jù)契耿,如果獲取不到就阻塞,如果獲取到就執(zhí)行螃征,其run()方法一直執(zhí)行)搪桂,這樣線程與任務(wù)個數(shù)比為m:n 其中m<<<n
因此,編寫多線程程序時盯滚,我們最好使用線程池踢械。
線程池的參數(shù)
- corePoolSize
核心線程數(shù),當(dāng)提交任務(wù)時如果線程數(shù)小于corePoolSize魄藕,則直接創(chuàng)建線程執(zhí)行該任務(wù)内列,否則,將任務(wù)添加到阻塞隊列
- maximumPoolSize
最大線程數(shù)背率,當(dāng)提交任務(wù)時话瞧,任務(wù)需添加到阻塞隊列且阻塞隊列滿時,如果線程數(shù)小于maximumPoolSize退渗,則創(chuàng)建線程執(zhí)行該任務(wù)移稳,否則執(zhí)行拒絕策略
注:如果阻塞隊列采用的是無界隊列的話,該參數(shù)無意義会油,因為阻塞隊列無界就永遠不會滿
- keepAliveTime
線程空閑時間个粱,空閑時間超過該時間則銷毀線程,只對大于corePoolSize~maximumPoolSize的線程有效翻翩,即至少保留corePoolSize個線程都许,即便空閑時間大于keepAliveTime也不銷毀。(核心線程也是可以銷毀的嫂冻,需要設(shè)置核心線程過期)
注:如果阻塞隊列為無界胶征,則maximumPoolSize無意義,那么keepAliveTime也就無意義
- unit
keepAliveTime的時間單位
- workQueue
阻塞隊列桨仿,分為有界隊列和無界隊列睛低,一般使用LinkedBlockingQueue、SynchronousQueue,用于存放任務(wù)钱雷,阻塞隊列的泛型必須是Runnable
- threadFactory
線程工廠骂铁,負(fù)責(zé)創(chuàng)建線程,指定線程名罩抗,線程組拉庵,線程優(yōu)先級,是否為守護線程等信息
- handler
拒絕策略套蒂,當(dāng)阻塞隊列放不下钞支,且線程數(shù)達到最大值maximumPoolSize時,再提交任務(wù)操刀,改任務(wù)會被拒絕烁挟。目前,JDK提供了四種拒絕策略
- CallerRunsPolicy 調(diào)用線程執(zhí)行策略馍刮,當(dāng)前執(zhí)行的線程執(zhí)行該任務(wù)信夫,可以保證任務(wù)不丟失,減緩任務(wù)添加的速度
- AbortPolicy 直接拋出異常卡啰,會導(dǎo)致線程池拋異常静稻,線程池不可用,默認(rèn)拒絕策略
- DiscardPolicy 直接丟棄該任務(wù)
- DiscardOldestPolicy 丟棄最老的任務(wù)匈辱,重試添加該任務(wù)
注:如果阻塞隊列為無界振湾,則拒絕策略無效,因為不會存在任務(wù)放不下的情況亡脸,也可以自定義自己的拒絕策略押搪。該參數(shù)一定要重視
線程池的構(gòu)造函數(shù)
構(gòu)造線程池?zé)o非就是為上節(jié)中介紹的幾個參數(shù)賦值,源碼如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
其他的構(gòu)造函數(shù)浅碾,都是間接調(diào)用該構(gòu)造函數(shù)
線程池的工作原理
- 提交任務(wù)大州,如何當(dāng)前線程數(shù)<corPoolSize,不管是否有空閑線程都會創(chuàng)建新的線程執(zhí)行
- 如何當(dāng)前線程數(shù)>=corPoolSize垂谢,將任務(wù)提交給阻塞隊列
- 如果阻塞隊列不滿厦画,添加到阻塞隊列,否則執(zhí)行4
- 如果當(dāng)前線程數(shù)<maxPoolSize,且不存在空閑線程則創(chuàng)建一個線程執(zhí)行該任務(wù)滥朱,否則執(zhí)行5
- 執(zhí)行拒絕策略
第一步需要注意的是在提交任務(wù)時根暑,excutor會不會判斷有無空閑線程,答案是不會徙邻,因為如果每次提交任務(wù)都需要判斷有無空閑線程排嫌,將會造成很大的開銷,excutor的做法是缰犁,啟動的每一個worker在空閑時都會去阻塞隊里阻塞的獲取任務(wù)淳地,如果沒有任務(wù)則worker會阻塞怖糊,因為worker到底空不空閑worker自己是最清楚的。
線程執(zhí)行完一個任務(wù)之后薇芝,會從阻塞隊列中獲取任務(wù)蓬抄,如果沒有任務(wù)可以獲取,則阻塞等待夯到,如果有任務(wù)則直接獲取執(zhí)行。與此同時饮亏,線程池中有專門的線程堅持線程的空閑時間(等待任務(wù)的時間)耍贾,如果超過指定時間且線程數(shù)>corePoolSize,就銷毀線程路幸。
Executors提供的三種線程池
- FixedThreadPool
固定大小的線程池荐开,其源代碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
通過源碼可以看出,線程池的corePoolSize和maximumPoolSize都為指定大小简肴,阻塞隊列使用無界阻塞隊列(看到無界阻塞隊列晃听,就應(yīng)該想到maximumPoolSize、keepAliveTime砰识、handler都無效)能扒,因此,該方法中有用的參數(shù)只有corePoolSize和workQueue是有意義的辫狼。
存在的問題:當(dāng)任務(wù)執(zhí)行的較慢初斑,且任務(wù)提交的速度過快時,會有大量的任務(wù)存放到阻塞隊列中膨处,阻塞隊列會越來越大见秤,內(nèi)存會被撐爆,使用該線程池時真椿,一定要考慮清楚鹃答。
除了該方法外,Executors還提供了重載方法突硝,可以指定ThreadFactory测摔,但是卻沒有提供修改阻塞隊列的重載方法
使用場景: 負(fù)載較重的服務(wù)器
- SingleThreadPool
當(dāng)個線程的線程池,與FixedThreadPool相比就是將線程數(shù)指定為1狞换,同樣該線程池存在FixedThreadPool存在的問題避咆,其源碼如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
與FixedThreadPool類型,Executors也提供了指定ThreadFactory的重載方法
使用場景: 單線程執(zhí)行環(huán)境修噪,保證順序執(zhí)行各個任務(wù)的場景
- CachedThreadPool
使用SynchronousQueue阻塞隊列查库,該隊列不保存元素,有任務(wù)提交到阻塞隊列時黄琼,任務(wù)必須立即被處理樊销。源碼如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
從源碼中可以看出整慎,maximumPoolSize的值為Integer.MAX_VALUE,意味著只要有任務(wù)到達围苫,且線程池內(nèi)沒有空閑線程裤园,就給任務(wù)開辟一個線程去執(zhí)行。線程空閑60s就銷毀
存在問題:如果任務(wù)執(zhí)行時間長剂府,提交速度快拧揽,那么會產(chǎn)生大量的線程,引起上下文切換腺占,應(yīng)用可能會出現(xiàn)假死或者崩潰的情況淤袜。
同樣,這種類型的線程池衰伯,也提供了一個指定ThreadFactory的重載方法
使用場景:適用于大量短期異步任務(wù)铡羡,或者負(fù)載較輕的服務(wù)器
由此可見:Executors提供的三種線程池都各自有優(yōu)缺點,如果使用線程池意鲸,建議不要使用這三種線程池烦周,而是直接通過線程池的構(gòu)造方法指定自己的corePoolSize,maximumPoolSize怎顾,keepAliveTime读慎,阻塞隊列workQueue,ThreadFactory杆勇,拒絕策略贪壳,自己指定的優(yōu)點就是可以根據(jù)自己的場景靈活的對各個參數(shù)進行配置。
線程池提交任務(wù)
- submit()
提交有返回值的任務(wù)蚜退,返回值為Future類型(真正的類型是RunnableFuture闰靴,而實現(xiàn)RunnableFuture接口的在JDK實現(xiàn)中對外可以使用的就只有FutureTask類
有關(guān)FutureTask的相關(guān)知識可以參考我的另外一篇文章: FutureTask原理講解與源碼剖析)
- execute()
提交沒有返回值的任務(wù)
線程池關(guān)閉
- shutdown()
將線程池的狀態(tài)修改為shutdown,禁止向線程池中提交任務(wù)钻注,并執(zhí)行完已經(jīng)提交的任務(wù)
- shutdonwNow()
將線程池的狀態(tài)修改為stop蚂且, 立即終止線程池中的線程, 不處理阻塞隊列中的任務(wù)幅恋,返回沒有執(zhí)行任務(wù)的列表
可以通過isTerminated()方法判斷線程池是否完全關(guān)閉
也可以通過awaitTermination(long timeout, TimeUnit unit)最長等待一段時間后退出杏死,但并不能保證關(guān)閉
如何分配線程池的大小
一般來講沒有上下文切換的多線程程序是最好的,因此捆交,如果有n個核淑翼,那么啟動n個線程就可以。但是線程并不是一直處于運行狀態(tài)(可能在等待IO放棄了cpu資源)品追,這樣cpu資源就會浪費玄括,因此我們一般針對不同的任務(wù)設(shè)定不同的線程數(shù)。
首先我們應(yīng)該獲取服務(wù)器的線程數(shù)肉瓦,可以通過如下代碼獲仍饩:
Runtime.getRuntime().availableProcessors();
注意胃惜,如果使用docker容器,使用該參數(shù)獲取的是實機的核數(shù)哪雕,并不是分配給docker容器的核數(shù)船殉,如果碰到需要修改, 具體情況具體分析斯嚎。
- 針對IO密集型任務(wù):一般分配2*p個線程(p代表服務(wù)器cpu總核數(shù))
- 針對cpu密集型任務(wù): 一般分配 p+1個線程
線程池的監(jiān)控
線程池提供了很多參數(shù)利虫,來記錄線程池中各個狀態(tài),了解即可:
- taskCount 線程池執(zhí)行任務(wù)總數(shù)
- completedTaskCount 已執(zhí)行完成任務(wù)數(shù)量
- largestPoolSize 創(chuàng)建過最大的線程數(shù)
- getPoolSize() 當(dāng)前線程數(shù)量
- getActiveCount() 活動線程數(shù)
除此之外堡僻,還可以繼承線程池類定義自己的線程池實現(xiàn)列吼, 可以重寫 beforeExecute(), afterExecute(), terminated()方法設(shè)置監(jiān)控
總結(jié)
本文并沒有從源碼的角度講解線程池,更加詳細(xì)的實現(xiàn)將在下一周抽時間整理苦始。