線程池是怎樣工作的


我們在工作中或多或少都使用過線程池,但是為什么要使用線程池呢?從他的名字中我們就應該知道,線程池使用了一種池化技術,和很多其他池化技術一樣,都是為了更高效的利用資源,例如鏈接池,內存池等等。

數據庫鏈接是一種很昂貴的資源,創建和銷毀都需要付出高昂的代價,為了避免頻繁的創建數據庫鏈接,所以產生了鏈接池技術。優先在池子中創建一批數據庫鏈接,有需要訪問數據庫時,直接到池子中去獲取一個可用的鏈接,使用完了之后再歸還到鏈接池中去。

同樣的,線程也是一種寶貴的資源,並且也是一種有限的資源,創建和銷毀線程也同樣需要付出不菲的代價。我們所有的代碼都是由一個一個的線程支撐起來的,如今的芯片架構也決定了我們必須編寫多線程執行的程序,以獲取最高的程序性能。

那么怎樣高效的管理多線程之間的分工與協作就成了一個關鍵問題,Doug Lea 大神為我們設計並實現了一款線程池工具,通過該工具就可以實現多線程的能力,並實現任務的高效執行與調度。

為了正確合理的使用線程池工具,我們有必要對線程池的原理進行了解。

本篇文章主要從三個方面來對線程池進行分析:線程池狀態、重要屬性、工作流程。

線程池狀態

首先線程池是有狀態的,這些狀態標識這線程池內部的一些運行情況,線程池的開啟到關閉的過程就是線程池狀態的一個流轉的過程。

線程池共有五種狀態:

thread-pool-executor-status.jpg

狀態 含義
RUNNING 運行狀態,該狀態下線程池可以接受新的任務,也可以處理阻塞隊列中的任務<br />執行 shutdown 方法可進入 SHUTDOWN 狀態<br />執行 shutdownNow 方法可進入 STOP 狀態
SHUTDOWN 待關閉狀態,不再接受新的任務,繼續處理阻塞隊列中的任務<br />當阻塞隊列中的任務為空,並且工作線程數為0時,進入 TIDYING 狀態
STOP 停止狀態,不接收新任務,也不處理阻塞隊列中的任務,並且會嘗試結束執行中的任務<br />當工作線程數為0時,進入 TIDYING 狀態
TIDYING 整理狀態,此時任務都已經執行完畢,並且也沒有工作線程<br />執行 terminated 方法后進入 TERMINATED 狀態
TERMINATED 終止狀態,此時線程池完全終止了,並完成了所有資源的釋放

重要屬性

一個線程池的核心參數有很多,每個參數都有着特殊的作用,各個參數聚合在一起后將完成整個線程池的完整工作。

1、線程狀態和工作線程數量

首先線程池是有狀態的,不同狀態下線程池的行為是不一樣的,5種狀態已經在上面說過了。

另外線程池肯定是需要線程去執行具體的任務的,所以在線程池中就封裝了一個內部類 Worker 作為工作線程,每個 Worker 中都維持着一個 Thread。

線程池的重點之一就是控制線程資源合理高效的使用,所以必須控制工作線程的個數,所以需要保存當前線程池中工作線程的個數。

看到這里,你是否覺得需要用兩個變量來保存線程池的狀態和線程池中工作線程的個數呢?但是在 ThreadPoolExecutor 中只用了一個 AtomicInteger 型的變量就保存了這兩個屬性的值,那就是 ctl。

ctl.jpg

ctl 的高3位用來表示線程池的狀態(runState),低29位用來表示工作線程的個數(workerCnt),為什么要用3位來表示線程池的狀態呢,原因是線程池一共有5種狀態,而2位只能表示出4種情況,所以至少需要3位才能表示得了5種狀態。

2、核心線程數和最大線程數

現在有了標志工作線程的個數的變量了,那到底該有多少個線程才合適呢?線程多了浪費線程資源,少了又不能發揮線程池的性能。

為了解決這個問題,線程池設計了兩個變量來協作,分別是:

  • 核心線程數:corePoolSize 用來表示線程池中的核心線程的數量,也可以稱為可閑置的線程數量
  • 最大線程數:maximumPoolSize 用來表示線程池中最多能夠創建的線程數量

現在我們有一個疑問,既然已經有了標識工作線程的個數的變量了,為什么還要有核心線程數、最大線程數呢?

其實你這樣想就能夠理解了,創建線程是有代價的,不能每次要執行一個任務時就創建一個線程,但是也不能在任務非常多的時候,只有少量的線程在執行,這樣任務是來不及處理的,而是應該創建合適的足夠多的線程來及時的處理任務。隨着任務數量的變化,當任務數明顯很小時,原本創建的多余的線程就沒有必要再存活着了,因為這時使用少量的線程就能夠處理的過來了,所以說真正工作的線程的數量,是隨着任務的變化而變化的。

那核心線程數和最大線程數與工作線程個數的關系是什么呢?

core-maximum-pool-size.jpg

工作線程的個數可能從0到最大線程數之間變化,當執行一段時間之后可能維持在 corePoolSize,但也不是絕對的,取決於核心線程是否允許被超時回收。

3、創建線程的工廠

既然是線程池,那自然少不了線程,線程該如何來創建呢?這個任務就交給了線程工廠 ThreadFactory 來完成。

4、緩存任務的阻塞隊列

上面我們說了核心線程數和最大線程數,並且也介紹了工作線程的個數是在0和最大線程數之間變化的。但是不可能一下子就創建了所有線程,把線程池裝滿,而是有一個過程,這個過程是這樣的:

當線程池接收到一個任務時,如果工作線程數沒有達到corePoolSize,那么就會新建一個線程,並綁定該任務,直到工作線程的數量達到 corePoolSize 前都不會重用之前的線程。

當工作線程數達到 corePoolSize 了,這時又接收到新任務時,會將任務存放在一個阻塞隊列中等待核心線程去執行。為什么不直接創建更多的線程來執行新任務呢,原因是核心線程中很可能已經有線程執行完自己的任務了,或者有其他線程馬上就能處理完當前的任務,並且接下來就能投入到新的任務中去,所以阻塞隊列是一種緩沖的機制,給核心線程一個機會讓他們充分發揮自己的能力。另外一個值得考慮的原因是,創建線程畢竟是比較昂貴的,不可能一有任務要執行就去創建一個新的線程。

所以我們需要為線程池配備一個阻塞隊列,用來臨時緩存任務,這些任務將等待工作線程來執行。

work-queue.jpg

5、非核心線程存活時間

上面我們說了當工作線程數達到 corePoolSize 時,線程池會將新接收到的任務存放在阻塞隊列中,而阻塞隊列又兩種情況:一種是有界的隊列,一種是無界的隊列。

如果是無界隊列,那么當核心線程都在忙的時候,所有新提交的任務都會被存放在該無界隊列中,這時最大線程數將變得沒有意義,因為阻塞隊列不會存在被裝滿的情況。

如果是有界隊列,那么當阻塞隊列中裝滿了等待執行的任務,這時再有新任務提交時,線程池就需要創建新的“臨時”線程來處理,相當於增派人手來處理任務。

但是創建的“臨時”線程是有存活時間的,不可能讓他們一直都存活着,當阻塞隊列中的任務被執行完畢,並且又沒有那么多新任務被提交時,“臨時”線程就需要被回收銷毀,在被回收銷毀之前等待的這段時間,就是非核心線程的存活時間,也就是 keepAliveTime 屬性。

那么什么是“非核心線程”呢?是不是先創建的線程就是核心線程,后創建的就是非核心線程呢?

其實核心線程跟創建的先后沒有關系,而是跟工作線程的個數有關,如果當前工作線程的個數大於核心線程數,那么所有的線程都可能是“非核心線程”,都有被回收的可能。

一個線程執行完了一個任務后,會去阻塞隊列里面取新的任務,在取到任務之前它就是一個閑置的線程。

取任務的方法有兩種,一種是通過 take() 方法一直阻塞直到取出任務,另一種是通過 poll(keepAliveTime,timeUnit) 方法在一定時間內取出任務或者超時,如果超時這個線程就會被回收,請注意核心線程一般不會被回收。

那么怎么保證核心線程不會被回收呢?還是跟工作線程的個數有關,每一個線程在取任務的時候,線程池會比較當前的工作線程個數與核心線程數:

  • 如果工作線程數小於當前的核心線程數,則使用第一種方法取任務,也就是沒有超時回收,這時所有的工作線程都是“核心線程”,他們不會被回收;
  • 如果大於核心線程數,則使用第二種方法取任務,一旦超時就回收,所以並沒有絕對的核心線程,只要這個線程沒有在存活時間內取到任務去執行就會被回收。

所以每個線程想要保住自己“核心線程”的身份,必須充分努力,盡可能快的獲取到任務去執行,這樣才能逃避被回收的命運。

核心線程一般不會被回收,但是也不是絕對的,如果我們設置了允許核心線程超時被回收的話,那么就沒有核心線程這種說法了,所有的線程都會通過 poll(keepAliveTime, timeUnit) 來獲取任務,一旦超時獲取不到任務,就會被回收,一般很少會這樣來使用,除非該線程池需要處理的任務非常少,並且頻率也不高,不需要將核心線程一直維持着。

6、拒絕策略

雖然我們有了阻塞隊列來對任務進行緩存,這從一定程度上為線程池的執行提供了緩沖期,但是如果是有界的阻塞隊列,那就存在隊列滿的情況,也存在工作線程的數據已經達到最大線程數的時候。如果這時候再有新的任務提交時,顯然線程池已經心有余而力不足了,因為既沒有空余的隊列空間來存放該任務,也無法創建新的線程來執行該任務了,所以這時我們就需要有一種拒絕策略,即 handler。

拒絕策略是一個 RejectedExecutionHandler 類型的變量,用戶可以自行指定拒絕的策略,如果不指定的話,線程池將使用默認的拒絕策略:拋出異常。

在線程池中還為我們提供了很多其他可以選擇的拒絕策略:

  • 直接丟棄該任務
  • 使用調用者線程執行該任務
  • 丟棄任務隊列中的最老的一個任務,然后提交該任務

工作流程

了解了線程池中所有的重要屬性之后,現在我們需要來了解下線程池的工作流程了。

how-thread-pool-work.jpg

上圖是一張線程池工作的精簡圖,實際的過程比這個要復雜的多,不過這些應該能夠完全覆蓋到線程池的整個工作流程了。

整個過程可以拆分成以下幾個部分:

1、提交任務

當向線程池提交一個新的任務時,線程池有三種處理情況,分別是:創建一個工作線程來執行該任務、將任務加入阻塞隊列、拒絕該任務。

提交任務的過程也可以拆分成以下幾個部分:

  • 當工作線程數小於核心線程數時,直接創建新的核心工作線程
  • 當工作線程數不小於核心線程數時,就需要嘗試將任務添加到阻塞隊列中去
  • 如果能夠加入成功,說明隊列還沒有滿,那么需要做以下的二次驗證來保證添加進去的任務能夠成功被執行
    • 驗證當前線程池的運行狀態,如果是非RUNNING狀態,則需要將任務從阻塞隊列中移除,然后拒絕該任務
    • 驗證當前線程池中的工作線程的個數,如果為0,則需要主動添加一個空工作線程來執行剛剛添加到阻塞隊列中的任務
  • 如果加入失敗,則說明隊列已經滿了,那么這時就需要創建新的“臨時”工作線程來執行任務
    • 如果創建成功,則直接執行該任務
    • 如果創建失敗,則說明工作線程數已經等於最大線程數了,則只能拒絕該任務了

整個過程可以用下面這張圖來表示:

execute-runnable.jpg

2、創建工作線程

創建工作線程需要做一系列的判斷,需要確保當前線程池可以創建新的線程之后,才能創建。

首先,當線程池的狀態是 SHUTDOWN 或者 STOP 時,則不能創建新的線程。

另外,當線程工廠創建線程失敗時,也不能創建新的線程。

還有就是當前工作線程的數量與核心線程數、最大線程數進行比較,如果前者大於后者的話,也不允許創建。

除此之外,會嘗試通過 CAS 來自增工作線程的個數,如果自增成功了,則會創建新的工作線程,即 Worker 對象。

然后加鎖進行二次驗證是否能夠創建工作線程,最后如果創建成功,則會啟動該工作線程。

3、啟動工作線程

當工作線程創建成功后,也就是 Worker 對象已經創建好了,這時就需要啟動該工作線程,讓線程開始干活了,Worker 對象中關聯着一個 Thread,所以要啟動工作線程的話,只要通過 worker.thread.start() 來啟動該線程即可。

啟動完了之后,就會執行 Worker 對象的 run 方法,因為 Worker 實現了 Runnable 接口,所以本質上 Worker 也是一個線程。

通過線程 start 開啟之后就會調用到 Runnable 的 run 方法,在 worker 對象的 run 方法中,調用了 runWorker(this) 方法,也就是把當前對象傳遞給了 runWorker 方法,讓他來執行。

4、獲取任務並執行

在 runWorker 方法被調用之后,就是執行具體的任務了,首先需要拿到一個可以執行的任務,而 Worker 對象中默認綁定了一個任務,如果該任務不為空的話,那么就是直接執行。

執行完了之后,就會去阻塞隊列中獲取任務來執行,而獲取任務的過程,需要考慮當前工作線程的個數。

  • 如果工作線程數大於核心線程數,那么就需要通過 poll 來獲取,因為這時需要對閑置的線程進行回收;
  • 如果工作線程數小於等於核心線程數,那么就可以通過 take 來獲取了,因此這時所有的線程都是核心線程,不需要進行回收,前提是沒有設置 allowCoreThreadTimeOut

逅弈逐碼,專注於原創分享,用通俗易懂的圖文描述源碼及原理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM