線程池的幾個靈魂拷問(一)

之前的博客里有寫過一點線程池吠架,但是只是蜻蜓點水式的談了一下,恰巧前段時間在工作中有了線程池的使用經(jīng)驗搂鲫,而且線程池的優(yōu)化又是一個比較有挑戰(zhàn)的難題傍药,所以這里借著實戰(zhàn)經(jīng)驗結合原理來一篇線程池的總結文章。


1.為什么要用線程池魂仍?

線程池解決的核心問題就是資源管理問題拐辽。在并發(fā)環(huán)境下,系統(tǒng)不能夠確定在任意時刻中擦酌,有多少任務需要執(zhí)行俱诸,有多少資源需要投入。這種不確定性將帶來以下若干問題:

  • 頻繁申請/銷毀資源和調(diào)度資源赊舶,將帶來額外的消耗睁搭,可能會非常巨大。
  • 對資源無限申請缺少抑制手段笼平,易引發(fā)系統(tǒng)資源耗盡的風險园骆。
  • 系統(tǒng)無法合理管理內(nèi)部的資源分布,會降低系統(tǒng)的穩(wěn)定性出吹。

為解決資源分配這個問題遇伞,線程池采用了“池化”(Pooling)思想。池化捶牢,顧名思義鸠珠,就是將資源統(tǒng)一在一起管理的一種思想。

2.線程池的核心參數(shù)

Java中的線程池核心實現(xiàn)類是ThreadPoolExecutor秋麸,ThreadPoolExecutor實現(xiàn)的頂層接口是Executor渐排,頂層接口Executor提供了一種思想:將任務提交和任務執(zhí)行進行解耦。用戶無需關注如何創(chuàng)建線程灸蟆,如何調(diào)度線程來執(zhí)行任務驯耻,用戶只需提供Runnable對象,將任務的運行邏輯提交到執(zhí)行器(Executor)中炒考,由Executor框架完成線程的調(diào)配和任務的執(zhí)行部分可缚。ThreadPoolExecutor,主要構造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:核心線程大小斋枢,當提交一個任務到線程池時帘靡,線程池會創(chuàng)建一個線程來執(zhí)行任務,即使有其他空閑線程可以處理任務也會創(chuàng)新線程瓤帚,等到工作的線程數(shù)大于核心線程數(shù)時就不會在創(chuàng)建了描姚。如果調(diào)用了線程池的prestartAllCoreThreads方法涩赢,線程池會提前把核心線程都創(chuàng)造好,并啟動

maximumPoolSize:線程池允許創(chuàng)建的最大線程數(shù)轩勘。如果隊列滿了筒扒,并且以創(chuàng)建的線程數(shù)小于最大線程數(shù),則線程池會再創(chuàng)建新的線程執(zhí)行任務绊寻。如果我們使用了無界隊列花墩,那么所有的任務會加入隊列,這個參數(shù)就沒有什么效果了

keepAliveTime:線程池的工作線程空閑后榛斯,保持存活的時間观游。如果沒有任務處理了搂捧,有些線程會空閑驮俗,空閑的時間超過了這個值,會被回收掉允跑。如果任務很多王凑,并且每個任務的執(zhí)行時間比較短,避免線程重復創(chuàng)建和回收聋丝,可以調(diào)大這個時間索烹,提高線程的利用率。

unit:keepAliveTIme的時間單位弱睦,可以選擇的單位有天百姓、小時、分鐘况木、毫秒垒拢、微妙、千分之一毫秒和納秒火惊。類型是一個枚舉java.util.concurrent.TimeUnit求类,這個枚舉也經(jīng)常使用,有興趣的可以看一下其源碼

workQueue:工作隊列屹耐,用于緩存待處理任務的阻塞隊列尸疆,常見的有4種,后面有介紹

threadFactory:線程池中創(chuàng)建線程的工廠惶岭,可以通過線程工廠給每個創(chuàng)建出來的線程設置更有意義的名字

handler:飽和策略寿弱,當線程池無法處理新來的任務了,那么需要提供一種策略處理提交的新任務按灶,默認有4種策略症革,文章后面會提到

線程池的簡單使用示例代碼:

public class Demo1 {
    static ThreadPoolExecutor executor = new ThreadPoolExecutor(3,
            5,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            int j = i;
            String taskName = "任務" + j;
            executor.execute(() -> {
                //模擬任務內(nèi)部處理耗時
                try {
                    TimeUnit.SECONDS.sleep(j);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + taskName + "處理完畢");
            });
        }
        //關閉線程池
        executor.shutdown();
    }
}

3、任務調(diào)度流程

  • 首先檢測線程池運行狀態(tài)兆衅,如果不是RUNNING地沮,則直接拒絕嗜浮,線程池要保證在RUNNING的狀態(tài)下執(zhí)行任務。
  • 如果workerCount < corePoolSize摩疑,則創(chuàng)建并啟動一個線程來執(zhí)行新提交的任務危融。
  • 如果workerCount >= corePoolSize,且線程池內(nèi)的阻塞隊列未滿雷袋,則將任務添加到該阻塞隊列中吉殃。
  • 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且線程池內(nèi)的阻塞隊列已滿楷怒,則創(chuàng)建并啟動一個線程來執(zhí)行新提交的任務蛋勺。
  • 如果workerCount >= maximumPoolSize,并且線程池內(nèi)的阻塞隊列已滿, 則根據(jù)拒絕策略來處理該任務, 默認的處理方式是直接拋異常鸠删。
任務調(diào)度策略

4.線程池中常見5種工作隊列

任務太多的時候抱完,工作隊列用于暫時緩存待處理的任務,JDK中常見的5種阻塞隊列:

  • ArrayBlockingQueue:是一個基于數(shù)組結構的有界阻塞隊列刃泡,此隊列按照先進先出原則對元素進行排序
  • LinkedBlockingQueue:是一個基于鏈表結構的阻塞隊列巧娱,此隊列按照先進先出排序元素,吞吐量通常要高于ArrayBlockingQueue烘贴。靜態(tài)工廠方法Executors.newFixedThreadPool使用了這個隊列禁添。
  • SynchronousQueue :一個不存儲元素的阻塞隊列,每個插入操作必須等到另外一個線程調(diào)用移除操作桨踪,否則插入操作一直處理阻塞狀態(tài)老翘,吞吐量通常要高于LinkedBlockingQueue,靜態(tài)工廠方法Executors.newCachedThreadPool使用這個隊列
  • PriorityBlockingQueue:優(yōu)先級隊列锻离,進入隊列的元素按照優(yōu)先級會進行排序

5.四種常見飽和策略

  • AbortPolicy:直接拋出異常
  • CallerRunsPolicy:在當前調(diào)用者的線程中運行任務铺峭,即隨丟來的任務,由他自己去處理
  • DiscardOldestPolicy:丟棄隊列中最老的一個任務纳账,即丟棄隊列頭部的一個任務逛薇,然后執(zhí)行當前傳入的任務
  • DiscardPolicy:不處理,直接丟棄掉疏虫,方法內(nèi)部為空

6.Executors類

Executors類永罚,提供了一系列工廠方法用于創(chuàng)建線程池,返回的線程池都實現(xiàn)了ExecutorService接口卧秘。常用的方法有:

  • newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

創(chuàng)建一個單線程的線程池呢袱。這個線程池只有一個線程在工作,也就是相當于單線程串行執(zhí)行所有任務翅敌。如果這個唯一的線程因為異常結束羞福,那么會有一個新的線程來替代它。此線程池保證所有任務的執(zhí)行順序按照任務的提交順序執(zhí)行蚯涮。內(nèi)部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務治专,任務如果比較多卖陵,單線程如果處理不過來,會導致隊列堆滿张峰,引發(fā)OOM泪蔫。

  • newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)

創(chuàng)建固定大小的線程池。每次提交一個任務就創(chuàng)建一個線程喘批,直到線程達到線程池的最大大小撩荣。線程池的大小一旦達到最大值就會保持不變,在提交新任務饶深,任務將會進入等待隊列中等待餐曹。如果某個線程因為執(zhí)行異常而結束,那么線程池會補充一個新線程敌厘。內(nèi)部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務台猴,任務如果比較多,如果處理不過來额湘,會導致隊列堆滿卿吐,引發(fā)OOM旁舰。

  • newCachedThreadPool
public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

創(chuàng)建一個可緩存的線程池锋华。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑(60秒處于等待任務到來)的線程箭窜,當任務數(shù)增加時毯焕,此線程池又可以智能的添加新線程來處理任務。此線程池的最大值是Integer的最大值(2^31-1)磺樱。內(nèi)部使用了SynchronousQueue同步隊列來緩存任務纳猫,此隊列的特性是放入任務時必須要有對應的線程獲取任務,任務才可以放入成功竹捉。如果處理的任務比較耗時芜辕,任務來的速度也比較快,會創(chuàng)建太多的線程引發(fā)OOM块差。

  • newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

創(chuàng)建一個大小無限的線程池侵续。此線程池支持定時以及周期性執(zhí)行任務的需求。

在《阿里巴巴java開發(fā)手冊》中指出了線程資源必須通過線程池提供憨闰,不允許在應用中自行顯示的創(chuàng)建線程状蜗,這樣一方面是線程的創(chuàng)建更加規(guī)范,可以合理控制開辟線程的數(shù)量鹉动;另一方面線程的細節(jié)管理交給線程池處理轧坎,優(yōu)化了資源的開銷。而線程池不允許使用Executors去創(chuàng)建泽示,而要通過ThreadPoolExecutor方式缸血,這一方面是由于jdk中Executor框架雖然提供了如newFixedThreadPool()蜜氨、newSingleThreadExecutor()、newCachedThreadPool()等創(chuàng)建線程池的方法捎泻,但都有其局限性记劝,不夠靈活;另外由于前面幾種方法內(nèi)部也是通過ThreadPoolExecutor方式實現(xiàn)族扰,使用ThreadPoolExecutor有助于大家明確線程池的運行規(guī)則厌丑,創(chuàng)建符合自己的業(yè)務場景需要的線程池,避免資源耗盡的風險渔呵。

1怒竿、JAVA線程池,這一篇就夠了
2扩氢、JUC中的Executor框架詳解1

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耕驰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子录豺,更是在濱河造成了極大的恐慌遍尺,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淋肾,死亡現(xiàn)場離奇詭異五续,居然都是意外死亡,警方通過查閱死者的電腦和手機咏花,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門趴生,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人昏翰,你說我怎么就攤上這事苍匆。” “怎么了棚菊?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵浸踩,是天一觀的道長。 經(jīng)常有香客問我统求,道長检碗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任球订,我火速辦了婚禮后裸,結果婚禮上,老公的妹妹穿的比我還像新娘冒滩。我一直安慰自己微驶,他們只是感情好,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著因苹,像睡著了一般苟耻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扶檐,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天凶杖,我揣著相機與錄音,去河邊找鬼款筑。 笑死智蝠,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的奈梳。 我是一名探鬼主播杈湾,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼攘须!你這毒婦竟也來了漆撞?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤于宙,失蹤者是張志新(化名)和其女友劉穎浮驳,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捞魁,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡至会,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了署驻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奋献。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖旺上,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糖埋,我是刑警寧澤宣吱,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站瞳别,受9級特大地震影響征候,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜祟敛,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一疤坝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧馆铁,春花似錦跑揉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽现拒。三九已至,卻和暖如春望侈,著一層夾襖步出監(jiān)牢的瞬間印蔬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工脱衙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侥猬,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓捐韩,卻偏偏與公主長得像陵究,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子奥帘,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355