通過源碼解析,深入Java 線程池原理

2020-12-13 計算機java編程

從池化技術到底層實現,一篇文章帶你貫通線程池技術。

1、池化技術簡介

在系統開發過程中,我們經常會用到池化技術來減少系統消耗,提升系統性能。

在編程領域,比較典型的池化技術有:

線程池、連接池、內存池、對象池等。

對象池通過復用對象來減少創建對象、垃圾回收的開銷;連接池(資料庫連接池、Redis連接池和HTTP連接池等)通過復用TCP連接來減少創建和釋放連接的時間。線程池通過復用線程提升性能。簡單來說,池化技術就是通過復用來提升性能。

線程、內存、資料庫的連接對象都是資源,在程序中,當你創建一個線程或者在堆上申請一塊內存的時候都涉及到很多的系統調用,也是非常消耗CPU的。如果你的程序需要很多類似的工作線程或者需要頻繁地申請釋放小塊內存,在沒有對這方面進行優化的情況下,這部分代碼很可能會成為影響你整個程序性能的瓶頸。

如果每次都是如此的創建線程->執行任務->銷毀線程,會造成很大的性能開銷。復用已創建好的線程可以提高系統的性能,藉助池化技術的思想,通過預先創建好多個線程,放在池中,這樣可以在需要使用線程的時候直接獲取,避免多次重複創建、銷毀帶來的開銷。

(1)線程池的優點

線程是稀缺資源,使用線程池可以減少創建和銷毀線程的次數,每個工作線程都可以重複使用。可以根據系統的承受能力,調整線程池中工作線程的數量,防止因為消耗過多內存導致伺服器崩潰。(2)線程池的風險

雖然線程池是構建多線程應用程式的強大機制,但使用它並不是沒有風險的。用線程池構建的應用程式容易遭受任何其它多線程應用程式容易遭受的所有並發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程洩漏。

死鎖任何多線程應用程式都有死鎖風險。當一組進程或線程中的每一個都在等待一個只有該組中另一個進程才能引起的事件時,我們就說這組進程或線程 死鎖了。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線程將永遠等下去。

資源不足線程池的一個優點在於:相對於其它替代調度機制(有些我們已經討論過)而言,它們通常執行得很好。但只有恰當地調整了線程池大小時才是這樣的。

線程消耗包括內存和其它系統資源在內的大量資源。除了

Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會為每個 Java

線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。

如果線程池太大,那麼被那些線程消耗的資源可能嚴重地影響系統性能。在線程之間進行切換將會浪費時間,而且使用超出比您實際需要的線程可能會引起資源匱乏問題,因為池線程正在消耗一些資源,而這些資源可能會被其它任務更有效地利用。

除了線程自身所使用的資源以外,服務請求時所做的工作可能需要其它資源,例如 JDBC 連接、套接字或文件,這些也都是有限資源,有太多的並發請求也可能引起失效,例如不能分配 JDBC 連接。

並發錯誤線程池和其它排隊機制依靠使用

wait() 和 notify()

方法,這兩個方法都難於使用。如果編碼不正確,那麼可能丟失通知,導致線程保持空閒狀態,儘管隊列中有工作要處理。使用這些方法時,必須格外小心;即便是專家也可能在它們上面出錯。而最好使用現有的、已經知道能工作的實現,例如在

util.concurrent 包。

線程洩漏各種類型的線程池中一個嚴重的風險是線程洩漏,當從池中除去一個線程以執行一項任務,而在任務完成後該線程卻沒有返回池時,會發生這種情況。發生線程洩漏的一種情形出現在任務拋出一個 RuntimeException 或一個 Error 時。

如果池類沒有捕捉到它們,那麼線程只會退出而線程池的大小將會永久減少一個。當這種情況發生的次數足夠多時,線程池最終就為空,而且系統將停止,因為沒有可用的線程來處理任務。

請求過載僅僅是請求就壓垮了伺服器,這種情況是可能的。在這種情形下,我們可能不想將每個到來的請求都排隊到我們的工作隊列,因為排在隊列中等待執行的任務可能會消耗太多的系統資源並引起資源缺乏。在這種情形下決定如何做取決於您自己;在某些情況下,您可以簡單地拋棄請求,依靠更高級別的協議稍後重試請求,您也可以用一個指出伺服器暫時很忙的響應來拒絕請求。

2、 如何配置線程池大小配置

一般需要根據任務的類型來配置線程池大小:

如果是CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為 NCPU+1如果是IO密集型任務,參考值可以設置為2*NCPU當然,這只是一個參考值,具體的設置還需要根據實際情況進行調整,比如可以先將線程池大小設置為參考值,再觀察任務運行情況和系統負載、資源利用率來進行適當調整。

3、線程池的底層原理

(1)線程池的狀態

線程池和線程一樣擁有自己的狀態,在ThreadPoolExecutor類中定義了一個volatile變量runState來表示線程池的狀態,線程池有四種狀態,分別為RUNNING、SHURDOWN、STOP、TERMINATED。

線程池創建後處於RUNNING狀態。調用shutdown後處於SHUTDOWN狀態,線程池不能接受新的任務,會等待緩衝隊列的任務完成。調用shutdownNow後處於STOP狀態,線程池不能接受新的任務,並嘗試終止正在執行的任務。當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷毀,任務緩存隊列已經清空或執行結束後,線程池被設置為TERMINATED狀態。

其中ctl這個AtomicInteger的功能很強大,其高3位用於維護線程池運行狀態,低29位維護線程池中線程數量

RUNNING:-1<<COUNT_BITS,即高3位為1,低29位為0,該狀態的線程池會接收新任務,也會處理在阻塞隊列中等待處理的任務

SHUTDOWN:0<<COUNT_BITS,即高3位為0,低29位為0,該狀態的線程池不會再接收新任務,但還會處理已經提交到阻塞隊列中等待處理的任務

STOP:1<<COUNT_BITS,即高3位為001,低29位為0,該狀態的線程池不會再接收新任務,不會處理在阻塞隊列中等待的任務,而且還會中斷正在運行的任務

TIDYING:2<<COUNT_BITS,即高3位為010,低29位為0,所有任務都被終止了,workerCount為0,為此狀態時還將調用terminated()方法

TERMINATED:3<<COUNT_BITS,即高3位為100,低29位為0,terminated()方法調用完成後變成此狀態

這些狀態均由int型表示,大小關係為 RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED,這個順序基本上也是遵循線程池從 運行 到 終止這個過程。

runStateOf(int c) 方法:c & 高3位為1,低29位為0的~CAPACITY,用於獲取高3位保存的線程池狀態workerCountOf(int c)方法:c & 高3位為0,低29位為1的CAPACITY,用於獲取低29位的線程數量ctlOf(int rs, int wc)方法:參數rs表示runState,參數wc表示workerCount,即根據runState和workerCount打包合併成ctl(2)為什麼ctl負責兩種角色

在Doug Lea的設計中,ctl負責兩種角色可以避免多餘的同步邏輯。

很多人會想,一個變量表示兩個值,就節省了存儲空間,但是這裡很顯然不是為了節省空間而設計的,即使將這輛個值拆分成兩個Integer值,一個線程池也就多了4個字節而已,為了這4個字節而去大費周章地設計一通,顯然不是Doug Lea的初衷。

在多線程的環境下,運行狀態和有效線程數量往往需要保證統一,不能出現一個改而另一個沒有改的情況,如果將他們放在同一個AtomicInteger中,利用AtomicInteger的原子操作,就可以保證這兩個值始終是統一的。

(3)線程池工作流程

預先啟動一些線程,線程無限循環從任務隊列中獲取一個任務進行執行,直到線程池被關閉。如果某個線程因為執行某個任務發生異常而終止,那麼重新創建一個新的線程而已,如此反覆。

一個任務從提交到執行完畢經歷過程如下:

第一步:如果當前線程池中的線程數目小於corePoolSize,則每來一個任務,就會創建一個線程去執行這個任務;

第二步:如果當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(一般來說是任務緩存隊列已滿),則會嘗試創建新的線程去執行這個任務;

第三步:如果線程池中的線程數量大於等於corePoolSize,且隊列workQueue已滿,但線程池中的線程數量小於maximumPoolSize,則會創建新的線程來處理被添加的任務

第四步:如果當前線程池中的線程數目達到maximumPoolSize,則會採取任務拒絕策略進行處理;

流程圖如下:

4、ThreadPoolExecutor解析

ThreadPoolExecutor繼承自AbstractExecutorService,同時實現了ExecutorService接口,也是Executor框架默認的線程池實現類,一般我們使用線程池,如沒有特殊要求,直接創建ThreadPoolExecutor,初始化一個線程池,如果需要特殊的線程池,則直接繼承ThreadPoolExecutor,並實現特定的功能,如ScheduledThreadPoolExecutor,它是一個具有定時執行任務的線程池。

(1)Executor框架

在深入源碼之前先來看看J.U.C包中的線程池類圖:

它們的最頂層是一個Executor接口,它只有一個方法:

它提供了一個運行新任務的簡單方法,Java線程池也稱之為Executor框架。

ExecutorService擴展了Executor,添加了操控線程池生命周期的方法,如shutDown(),shutDownNow()等,以及擴展了可異步跟蹤執行任務生成返回值Future的方法,如submit()等方法。

(2)Worker解析

Worker類繼承了AQS,並實現了Runnable接口,它有兩個重要的成員變量:firstTask和thread。firstTask用於保存第一次新建的任務;thread是在調用構造方法時通過ThreadFactory來創建的線程,是用來處理任務的線程。

需要注意workers的數據結構為HashSet,非線程安全,所以操作workers需要加同步鎖。添加步驟做完後就啟動線程來執行任務了。

(3)如何在線程池中添加任務

線程池要執行任務,那麼必須先添加任務,execute()雖說是執行任務的意思,但裡面也包含了添加任務的步驟在裡面,下面源碼:

addWorker添加任務,方法源碼有點長,按照邏輯拆分成兩部分講解:

java.util.concurrent.ThreadPoolExecutor#addWorker:

這裡特別強調,firstTask是開啟線程執行的首個任務,之後常駐在線程池中的線程執行的任務都是從阻塞隊列中取出的,需要注意。

以上for循環代碼主要作用是判斷ctl變量當前的狀態是否可以添加任務,特別說明了如果線程池處於SHUTDOWN狀態時,可以繼續執行阻塞隊列中的任務,但不能繼續往線程池中添加任務了;同時增加工作線程數量使用了AQS作同步,如果同步失敗,則繼續循環執行。

以上源碼主要的作用是創建一個Worker對象,並將新的任務裝進Worker中,開啟同步將Worker添加進workers中,這裡需要注意workers的數據結構為HashSet,非線程安全,所以操作workers需要加同步鎖。添加步驟做完後就啟動線程來執行任務了,繼續往下看。

(4)前置和後置鉤子

如果需要在任務執行前後插入邏輯,你可以實現ThreadPoolExecutor以下兩個方法:

這樣一來,就可以對任務的執行進行實時監控。

5、線程池總結

線程池原理關鍵技術:鎖(lock,cas)、阻塞隊列、hashSet(資源池)

所謂線程池本質是一個Worker對象的hashSet,多餘的任務會放在阻塞隊列中,只有當阻塞隊列滿了後,才會觸發非核心線程的創建,非核心線程只是臨時過來打雜的,直到空閒,然後自己關閉。

線程池提供了兩個鉤子(beforeExecute,afterExecute)給我們,我們繼承線程池,在執行任務前後做一些事情。

相關焦點

  • Java中線程池,你真的會用嗎?
    深入源碼分析Java線程池的實現原理》這篇文章中,我們介紹過了Java中線程池的常見用法以及基本原理。在文中有這樣一段描述:可以通過Executors靜態工廠構建線程池,但一般不建議這樣使用。關於這個問題,在那篇文章中並沒有深入的展開。作者之所以這麼說,是因為這種創建線程池的方式有很大的隱患,稍有不慎就有可能導致線上故障。本文我們就來圍繞這個問題來分析一下為什麼JDK自身提供的構建線程池的方式並不建議使用?
  • 帶你一步步從源碼角度深入理解Java線程池(簡單易懂)
    使用場景分析(1) ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常這是線程池默認的拒絕策略,在任務不能再提交的時候會拋出異常,及時反饋程序運行狀態。如果是比較關鍵的業務,推薦使用此拒絕策略,這樣在系統不能承載更大並發量的時候,能夠及時的通過異常發現問題。
  • 深入理解 Java 線程池!
    這種問題使用線程池便可以很好的解決。通過線程池線程,銷毀及回收等交由線程池進行管理,就可以避免以上的問題。我們在使用過程中經常會直接使用newSingleThreadExecutor(),newCachedThreadPool(),newFixedThreadPool(int Threads)等已經封裝好的線程池,但這些都是通過ThreadPoolExecutor類中通過構造函數傳入不同的參數封裝的對象,所以想要了解線程池,我們就要認真研究一下線程池中最重要的ThreadPoolExecutor類。
  • Java8線程池ThreadPoolExecutor底層原理及其源碼解析
    提交任務到線程池中的流程3.1 ThreadPoolExecutor#execute方法整體流程這裡以java.util.concurrent.ThreadPoolExecutor#execute方法為例, 畫一個簡單的圖:上圖中的worker可簡單理解為線程池中的一個線程, workers.size
  • JAVA 線程池ThreadPoolExcutor原理探究
    Java 中的線程池是用 ThreadPoolExecutor 類來實現的. 本文就對該類的源碼來分析一下這個類內部對於線程的創建, 管理以及後臺任務的調度等方面的執行原理。下面將進入源碼分析,來深入理解 ThreadPoolExeCutor 的設計思想。 構造函數先來看構造函數:
  • Java從源碼角度分析創建線程池究竟有哪些方式
    前言在Java的高並發領域,線程池一直是一個繞不開的話題。有些童鞋一直在使用線程池,但是,對於如何創建線程池僅僅停留在使用Executors工具類的方式,那麼,創建線程池究竟存在哪幾種方式呢?就讓我們一起從創建線程池的源碼來深入分析究竟有哪些方式可以創建線程池。
  • Java中線程池的簡單使用
    線程池就可以幫助我們解決這個問題,他使線程可以重複使用,就是執行完一個任務線程不會被銷毀,而是可以繼續執行其他任務java中的線程池如何使用?// 創建一個容量為5的線程池 ExecutorService executorService = Executors.newFixedThreadPool(5); // 向線程池提交一個任務(其實就是通過線程池來啟動一個線程)
  • 阿里P9都窺視已久的「Java並發實現原理:JDK源碼剖析」
    而從JDK 1.5開始,並發編程大師Doug Lea奉上了一個系統而全面的並發編程框架——JDK Concurrent包,裡面包含了各種原子操作、線程安全的容器、線程池和異步編程等內容。本書基於JDK 7和JDK 8,對整個Concurrent包進行全面的源碼剖析。JDK 8中大部分並發功能的實現和JDK 7一樣,但新增了一些額外特性。
  • Java之線程池的簡單介紹
    通過使用線程池,可以使線程復用,就是執行完一個任務,不銷毀,繼續執行其他任務。其實線程池就相當於一個容器(集合),裡面有很多線程。線程池原理圖解其實線程池就是一個容納多線程的容器,其中線程可以反覆使用,省去了創建線程對象的操作,無需反覆創建線程而消耗過多資源。
  • 使用Executors,ThreadPoolExecutor,創建線程池,源碼分析理解
    源碼分析public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue
  • 拼多多JDK源碼大揭秘,由淺入深看源碼,探究Java並發原理
    寫在前面幾乎所有的大神都會強調看源碼,也強調源碼的重要性;但是如何看源碼,源碼看什麼?看了什麼用?看了怎麼用?困擾很多人,尤其是初學者。如何閱讀源碼,是每個程式設計師需要面臨的一項挑戰。為什麼需要閱讀源碼?
  • 深入理解 Java 線程池,講解的太清晰了
    正是由於這個問題,所以有必要引入線程池。使用 線程池的好處有以下幾點:降低資源消耗 - 通過重複利用已創建的線程降低線程創建和銷毀造成的消耗。提高線程的可管理性 - 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。但是要做到合理的利用線程池,必須對其原理了如指掌。
  • java新手揭秘:阿里巴巴為何禁止使用Executors來創建線程池
    當一個java新手從不斷地Curd階段跳出來之後,就會學習java的並發,並行等高階用法,自然就會用到線程、線程池,線程池的好處這裡就不做詳細解釋,你應該會學習到Executors創建線程池的四個方法, 分別是:newFixedThreadPool
  • 阿里P8架構師力薦的 Java源碼解析及面試合集
    源碼解析和設計思路06 LinkedList 源碼解析07 List 源碼會問哪些面試題08 HasMap源碼解析19 LinkedBlockingQueue 源碼解析20 SynchronousQueue 源碼解析21 DelayQueue 源碼解析
  • Java中的線程池實現原理
    Java中的線程池實現原理上文從自定義一個線程池Java線程池,簡單說了線程的實現原理,本文就從Java實現的線程池說說線程池的原理。線程池的實現原理首先創建一個線程池,當添加一個新的任務的時候,線程池的處理流程。
  • Java線程池其實看懂了也很簡單
    如果臨時工達到最大數且隊列也滿了,那麼我們只能通過拒絕策略暫時不接受額外的服務要求了。4|0一起看源碼口說無憑,理論都是這樣說的,那實際上源碼是不是真是這樣寫的呢?我們一起來看下線程池的源碼。通過 threadPoolExecutor.execute(...)的入口進入源碼,刪除了注釋信息之後的源碼內容如下,由於封裝的好,所以只有短短幾行。如果不關注細節只關注整體,從以上源碼中我們可以發現其中主要分為了四個步驟來處理邏輯。排除第一步的非空校驗代碼,我們可以看出剩下的三步其實就是我們線程池的運行邏輯,也就是上面的運行流程圖的邏輯內容。
  • 從使用到原理,探究Java線程池
    2.幾種常見的線程池分析Java為我們提供了幾種常用的線程池,通過Executors類可以輕易地獲取它們。下面我們通過分析這幾種常用線程池的參數,了解這些線程池之間的異同。通過阻塞隊列,這個線程池能夠保證任務是按順序執行的。
  • 【線程池】java線程池ThreadPoolExecutor
    可選的參數為java.util.concurrent.TimeUnit中的幾個靜態屬性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。    workQueue 存放通過execute方法提交給線程等待執行的任務的隊列。    threadFactory 負責給線程池創建線程的工廠。
  • Java並發編程系列34|深入理解線程池(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文是深入理解線程池下篇:線程池介紹Executor框架接口線程池狀態線程池參數線程池創建執行過程關閉線程池其他問題任務拒絕策略線程池中的線程初始化線程池容量的動態調整線程池的監控6.
  • Java實現終止線程池中正在運行的定時任務
    但是我們項目的需求完全是多線程的模型啊,而timer是單線程的,so,樓主最後還是選擇了jdk的線程池。線程池是什麼Java通過Executors提供四種線程池,分別為:**newCachedThreadPool :**創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。