《線程池系列二》-ThreadPoolExecutor-線程池原理解析

相信大家都使用過線程池娜睛,也了解使用線程池的好處。我們使用線程池最多的還是使用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類具有兩個功能:

  1. 維護線程 線程的創(chuàng)建、休眠派殷、中斷还最、暫停、銷毀
  2. 執(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提供了四種拒絕策略

  1. CallerRunsPolicy 調(diào)用線程執(zhí)行策略馍刮,當(dāng)前執(zhí)行的線程執(zhí)行該任務(wù)信夫,可以保證任務(wù)不丟失,減緩任務(wù)添加的速度
  2. AbortPolicy 直接拋出異常卡啰,會導(dǎo)致線程池拋異常静稻,線程池不可用,默認(rèn)拒絕策略
  3. DiscardPolicy 直接丟棄該任務(wù)
  4. 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ù)

線程池的工作原理

  1. 提交任務(wù)大州,如何當(dāng)前線程數(shù)<corPoolSize,不管是否有空閑線程都會創(chuàng)建新的線程執(zhí)行
  2. 如何當(dāng)前線程數(shù)>=corPoolSize垂谢,將任務(wù)提交給阻塞隊列
  3. 如果阻塞隊列不滿厦画,添加到阻塞隊列,否則執(zhí)行4
  4. 如果當(dāng)前線程數(shù)<maxPoolSize,且不存在空閑線程則創(chuàng)建一個線程執(zhí)行該任務(wù)滥朱,否則執(zhí)行5
  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ù)船殉,如果碰到需要修改, 具體情況具體分析斯嚎。

  1. 針對IO密集型任務(wù):一般分配2*p個線程(p代表服務(wù)器cpu總核數(shù))
  2. 針對cpu密集型任務(wù): 一般分配 p+1個線程

線程池的監(jiān)控

線程池提供了很多參數(shù)利虫,來記錄線程池中各個狀態(tài),了解即可:

  1. taskCount 線程池執(zhí)行任務(wù)總數(shù)
  2. completedTaskCount 已執(zhí)行完成任務(wù)數(shù)量
  3. largestPoolSize 創(chuàng)建過最大的線程數(shù)
  4. getPoolSize() 當(dāng)前線程數(shù)量
  5. getActiveCount() 活動線程數(shù)

除此之外堡僻,還可以繼承線程池類定義自己的線程池實現(xiàn)列吼, 可以重寫 beforeExecute(), afterExecute(), terminated()方法設(shè)置監(jiān)控

總結(jié)

本文并沒有從源碼的角度講解線程池,更加詳細(xì)的實現(xiàn)將在下一周抽時間整理苦始。

歡迎掃描下方二維碼,關(guān)注公眾號慌申,我們可以進行技術(shù)交流陌选,共同成長

qrcode_for_gh_5580beb3cba1_430.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蹄溉,隨后出現(xiàn)的幾起案子咨油,更是在濱河造成了極大的恐慌,老刑警劉巖柒爵,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件役电,死亡現(xiàn)場離奇詭異,居然都是意外死亡棉胀,警方通過查閱死者的電腦和手機法瑟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來唁奢,“玉大人霎挟,你說我怎么就攤上這事÷榈В” “怎么了酥夭?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脊奋。 經(jīng)常有香客問我熬北,道長,這世上最難降的妖魔是什么诚隙? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任讶隐,我火速辦了婚禮,結(jié)果婚禮上最楷,老公的妹妹穿的比我還像新娘整份。我一直安慰自己待错,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布烈评。 她就那樣靜靜地躺著火俄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讲冠。 梳的紋絲不亂的頭發(fā)上瓜客,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音竿开,去河邊找鬼谱仪。 笑死,一個胖子當(dāng)著我的面吹牛否彩,可吹牛的內(nèi)容都是我干的疯攒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼列荔,長吁一口氣:“原來是場噩夢啊……” “哼敬尺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贴浙,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤砂吞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后崎溃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜻直,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年袁串,在試婚紗的時候發(fā)現(xiàn)自己被綠了概而。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡般婆,死狀恐怖到腥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔚袍,我是刑警寧澤乡范,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站啤咽,受9級特大地震影響晋辆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宇整,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一瓶佳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鳞青,春花似錦霸饲、人聲如沸为朋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽习寸。三九已至,卻和暖如春傻工,著一層夾襖步出監(jiān)牢的瞬間霞溪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工中捆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸯匹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓泄伪,卻偏偏與公主長得像殴蓬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蟋滴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容