天天看點

一文詳解線程池的工作原理

作者:BUG弄潮兒

在工作中或多或少都使用過線程池。但是為什麼要使用線程池呢?從它的名稱中我們就可以猜到,線程池是使用了一種池化技術(Pooling Technology)。和很多其他池化技術一樣,都是為了更高效的利用資源,例如連接配接池,記憶體池等。

資料庫連接配接是一種很昂貴的資源,建立和銷毀都需要付出高昂的代價。為了避免頻繁地建立資料庫連接配接,是以産生了資料庫連接配接池技術。優先在池子中建立一批資料庫連接配接,當有需要通路資料庫時,直接到池子中去擷取一個可用的連接配接,使用完了之後再歸還到連接配接池中去。

同樣的,線程也是一種很寶貴的資源,并且也是一種有限的資源,建立和銷毀線程也同樣需要付出不菲的代價。我們所有的代碼執行都是由一個一個的線程支撐起來的,如今的晶片架構也決定了我們必須編寫多線程執行的程式,以獲得最高的程式性能。那麼怎樣高效地管理多線程之間的分工與協作就成了一個關鍵問題,Doug Lea大神為我們設計并實作了一款線程池工具,通過該工具就可以實作多線程的能力,并實作任務的高效執行與排程。為了正确合理地使用線程池工具,我們有必要對線程池的原理進行了解。

了解線程池工作原理主要有三個方面:線程池狀态、線程池的重要屬性和線程池的工作流程。

線程池狀态

線程池是有狀态的,這些狀态辨別這個線程池内部的一些運作情況。線程池的開啟到關閉的過程就是線程池狀态的一個流轉過程。

線程池共有5種狀态:

一文詳解線程池的工作原理
  • 運作狀态(RUNNING):此狀态下,線程池可以接受新的任務,也可以處理阻塞隊列中的任務。執行shutdown()方法可進入待關閉(SHUTDOWN)狀态,執行shutdownNow()方法可進入停止(STOP)狀态。
  • 待關閉狀态(SHUTDOWN):此狀态下,線程池不再接受新的任務,繼續處理阻塞隊列中的任務。當阻塞隊列中的任務為空,且工作線程數為0的時候,進入整理(TIDYING)狀态。
  • 停止狀态(STOP):此狀态下,線程池不接受新任務,也不處理阻塞隊列中的任務,反而會嘗試結束執行中的任務。當工作線程數為0時,進入整理(TIDYING)狀态。
  • 整理狀态(TIDYING):此狀态下,所有任務都已經執行完畢,且沒有工作線程。執行terminated()方法進入終止(TERMINATED)狀态。
  • 終止狀态(TERMINATED):此狀态下,線程池完全終止,并完成了所有資源的釋放。

線程池的重要屬性

一個線程池的核心參數有很多,每個參數都有着特殊的作用,各個參數聚合再一起後将完成整個線程池的完整工作。其中的六個尤為重要:線程狀态和工作線程的數量,核心線程數和最大線程數,建立線程的工廠,緩存任務的阻塞隊列,非核心線程存活的時間和拒絕政策。

線程狀态和工作線程數量

首先線程池是有狀态的,在不同的狀态下,線程池的行為是不一樣的。

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

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

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

一文詳解線程池的工作原理

ctl是一個原子操作類型(AtomicInteger)的變量。ctl的高3位用來表示線程池的狀态(runState),低29位用來表示工作線程的個數(workerCnt)。為什麼要用3位來表示線程池的狀态呢,原因是因為線程池一共有5種狀态,而2位隻能表示出4種情況(2位是2^2,最多産生4種結果),至少需要3位才能表示得了全部的5種狀态(3位是3^2,最多産生9種結果)。

核心線程數和最大線程數

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

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

核心線程數(corePoolSize):用來表示線程池中的核心線程的數量,也可以稱為可閑置的線程數量。

最大線程數(maximumPoolSize):用來表示線程池中最多能夠建立的線程數量。

現在我們有一個疑惑,既然已經有了辨別工作線程的個數的變量了,為什麼還要有核心線程數和最大線程數呢?

其實你這樣想就能夠了解了,建立線程是有代價的,不能每次要執行一個任務時就建立一個線程,但是也不能在任務非常多的時候,隻有少量的線程在執行,這樣任務是來不及處理的,而是應該建立合适的足夠多的線程來及時地處理任務。

随着任務數量的變化,當任務數量明顯減少時,原本建立的多餘的線程就沒有必要再存活着了,因為這時使用少量的線程就能夠處理得過來了,是以說真正工作的線程的數量,是随着任務的變化而變化的。

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

一文詳解線程池的工作原理

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

建立線程的工廠

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

緩存任務的阻塞隊列

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

當線程池接受到一個任務時,如果工作線程數沒有達到corePoolSize,那麼就會建立一個線程,并綁定該任務,知道工作線程的數量達到corePoolSize前都不會重用之前建立的線程。

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

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

一文詳解線程池的工作原理

非核心線程存活時間

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

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

如果是有界隊列,那麼當阻塞隊列中裝滿了等待執行的任務,這時再有新任務送出時,線程池就需要建立新的臨時線程來處理,相當于增派人手來處理任務。

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

那麼什麼是非核心線程呢?是不是先建立的線程就是核心線程,後建立的就是非核心線程呢?

其實核心線程跟建立的先後沒有關系,而是跟工作線程的個數有關,如果目前工作線程的個數大于核心線程數,那麼所有的線程都可能是非核心線程,都有被回收的可能。

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

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

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

  1. 如果工作線程數小于目前的核心線程數,則使用第一種方法取任務,也就是沒有逾時回收,這時所有的工作線程都是核心線程,它們不會被回收。
  2. 如果工作線程數大于核心線程數,則使用第二種方法取任務,一旦逾時就回收,是以并沒有絕對的核心線程,隻要這個線程沒有在存活時間内取到任務去執行就會被回收。

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

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

拒絕政策

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

拒絕政策是一個RejectedExecutionHandler類型的變量,使用者可以自行指定拒絕的政策,如果不指定的話,線程池将使用預設的拒絕政策:抛出異常。

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

  1. 直接丢棄該任務
  2. 使用調用者線程執行該任務
  3. 丢棄任務隊列中的最老的一個任務,然後送出該任務

工作流程

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

一文詳解線程池的工作原理

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

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

送出任務

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

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

  1. 當工作線程數小于核心線程數時,直接建立新的核心工作線程。
  2. 當工作線程數大于核心線程數時,就需要嘗試将任務添加到阻塞隊列中去。
  3. 如果能夠加入成功,說明隊列還沒滿,那麼就需要做以下的二次校驗來保證添加進去的任務能夠成功被執行。
  4. 驗證目前線程池中的運作狀态,如果是非RUNNING狀态,則需要将任務從阻塞隊列中移除,然後拒絕該任務。
  5. 驗證目前線程池中的工作線程的個數,如果是0,則需要主動添加一個空工作線程來執行剛剛添加到阻塞隊列中的任務。
  6. 如果加入失敗,說明隊列已經滿了,這時就需要建立新的臨時工作線程來執行任務。
  7. 如果建立成功,則直接執行該任務。
  8. 如果建立失敗,說明工作線程數已經等于最大線程數了,隻能拒絕該任務了。

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

一文詳解線程池的工作原理

建立工作線程

建立工作線程需要做一系列的判斷,需要確定目前線程池可以建立新的線程之後,才能建立。

  • 首先,當線程池的狀态是SHUTDOWN或者STOP時,不能建立新的線程。
  • 其次,當線程工廠建立線程失敗時,也不能建立新的線程。
  • 第三,拿目前工作線程的數量與核心線程數、最大線程數進行比較,如果前者大于後者的話,也不允許建立。

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

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

啟動工作線程

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

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

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

擷取任務并執行

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

執行完了之後,就會去阻塞隊列中擷取任務來執行。

擷取任務的過程則需要考慮目前工作線程的個數:

  1. 如果工作線程數大于核心線程數,那麼就需要通過poll(keepAliveTime, timeUnit)來擷取,因為這時需要對閑置線程進行逾時回收。
  2. 如果工作線程數小于等于核心線程數,那麼就可以通過take()來擷取了。因為這時所有的線程都是核心線程,不需要進行回收,前提是沒有設定allowCoreThreadTimeOut(允許核心線程逾時回收)為true。
source: www.cnblogs.com/yanggb/p/10629387.html           

繼續閱讀