執行緒池中你不容錯過的一些細節

crossoverJie發表於2019-03-26

背景

上週分享了一篇《一個執行緒罷工的詭異事件》,最近也在公司內部分享了這個案例。

無獨有偶,在內部分享的時候也有小夥伴問了之前分享時所提出的一類問題:

執行緒池中你不容錯過的一些細節

執行緒池中你不容錯過的一些細節

執行緒池中你不容錯過的一些細節

執行緒池中你不容錯過的一些細節

這其實是一類共性問題,我認為主要還是兩個原因:

  • 我自己確實也沒講清楚,之前畫的那張圖還需要再完善,有些誤導。
  • 第二還是大家對執行緒池的理解不夠深刻,比如今天要探討的內容。

執行緒池的工作原理

首先還是來複習下執行緒池的基本原理。

我認為執行緒池它就是一個排程任務的工具。

眾所周知在初始化執行緒池會給定執行緒池的大小,假設現在我們有 1000 個執行緒任務需要執行,而執行緒池的大小為 10~20,在真正執行任務的過程中他肯定不會建立這1000個執行緒同時執行,而是充分利用執行緒池裡這 10~20 個執行緒來排程這1000個任務。

而這裡的 10~20 個執行緒最後會由執行緒池封裝為 ThreadPoolExecutor.Worker 物件,而這個 Worker 是實現了 Runnable 介面的,所以他自己本身就是一個執行緒。

深入分析

執行緒池中你不容錯過的一些細節

這裡我們來做一個模擬,建立了一個核心執行緒、最大執行緒數、阻塞佇列都為2的執行緒池。

這裡假設執行緒池已經完成了預熱,也就是執行緒池內部已經建立好了兩個執行緒 Worker

當我們往一個執行緒池丟一個任務會發生什麼事呢?

執行緒池中你不容錯過的一些細節

  • 第一步是生產者,也就是任務提供者他執行了一個 execute() 方法,本質上就是往這個內部佇列裡放了一個任務。
  • 之前已經建立好了的 Worker 執行緒會執行一個 while 迴圈 ---> 不停的從這個內部佇列裡獲取任務。(這一步是競爭的關係,都會搶著從佇列裡獲取任務,由這個佇列內部實現了執行緒安全。)
  • 獲取得到一個任務後,其實也就是拿到了一個 Runnable 物件(也就是 execute(Runnable task) 這裡所提交的任務),接著執行這個 Runnablerun() 方法,而不是 start(),這點需要注意後文分析原因。

結合原始碼來看:

執行緒池中你不容錯過的一些細節

從圖中其實就對應了剛才提到的二三兩步:

  • while 迴圈,從 getTask() 方法中一直不停的獲取任務。
  • 拿到任務後,執行它的 run() 方法。

這樣一個執行緒就排程完畢,然後再次進入迴圈從佇列裡取任務並不斷的進行排程。

再次解釋之前的問題

接下來回顧一下我們上一篇文章所提到的,導致一個執行緒沒有執行的根本原因是:

在單個執行緒的執行緒池中一但丟擲了未被捕獲的異常時,執行緒池會回收當前的執行緒並建立一個新的 Worker; 它也會一直不斷的從佇列裡獲取任務來執行,但由於這是一個消費執行緒,根本沒有生產者往裡邊丟任務,所以它會一直 waiting 在從佇列裡獲取任務處,所以也就造成了線上的佇列沒有消費,業務執行緒池沒有執行的問題。

結合之前的那張圖來看:

執行緒池中你不容錯過的一些細節

這裡大家問的最多的一個點是,為什麼會沒有是根本沒有生產者往裡邊丟任務,圖中不是明明畫的有一個 product 嘛?

這裡確實是有些不太清楚,再次強調一次:

圖中的 product 是往內部佇列裡寫訊息的生產者,並不是往這個 Consumer 所在的執行緒池中寫任務的生產者。

因為即便 Consumer 是一個單執行緒的執行緒池,它依然具有一個常規執行緒池所具備的所有條件:

  • Worker 排程執行緒,也就是執行緒池執行的執行緒;雖然只有一個。
  • 內部的阻塞佇列;雖然長度只有1。

再次結合圖來看:

執行緒池中你不容錯過的一些細節

所以之前提到的【沒有生產者往裡邊丟任務】是指右圖放大後的那一塊,也就是內部佇列並沒有其他執行緒往裡邊丟任務執行 execute() 方法。

而一旦發生未捕獲的異常後,Worker1 被回收,順帶的它所排程的執行緒 task1(這個task1 也就是在執行一個 while 迴圈消費左圖中的那個佇列) 也會被回收掉。

新建立的 Worker2 會取代 Worker1 繼續執行 while 迴圈從內部佇列裡獲取任務,但此時這個佇列就一直會是空的,所以也就是處於 Waiting 狀態。

我覺得這波解釋應該還是講清楚了,歡迎還沒搞明白的朋友留言討論。

為什是 run() 而不是 start()

問題搞清楚後來想想為什麼執行緒池在排程的時候執行的是 Runnablerun() 方法,而不是 start() 方法呢?

我相信大部分沒有看過原始碼的同學心中第一個印象就應該是執行的 start() 方法;

因為不管是學校老師,還是網上大牛講的都是隻有執行了start() 方法後作業系統才會給我們建立一個獨立的執行緒來執行,而 run() 方法只是一個普通的方法呼叫。

而線上程池這個場景中卻恰好就是要利用它只是一個普通方法呼叫

回到我在文初中所提到的:我認為執行緒池它就是一個排程任務的工具。

假設這裡是呼叫的 Runnablestart 方法,那會發生什麼事情。

如果我們往一個核心、最大執行緒數為 2 的執行緒池裡丟了 1000 個任務,那麼它會額外的建立 1000 個執行緒,同時每個任務都是非同步執行的,一下子就執行完畢了

從而沒法做到由這兩個 Worker 執行緒來排程這 1000 個任務,而只有當做一個同步阻塞的 run() 方法呼叫時才能滿足這個要求。

這事也讓我發現一個奇特的現象:就是網上幾乎沒人講過為什麼線上程池裡是 run 而不是 start,不知道是大家都覺得這是基操還是沒人仔細考慮過。

總結

針對之前線上事故的總結上次已經寫得差不多了,感興趣的可以翻回去看看。

這次呢可能更多是我自己的總結,比如寫一篇技術部落格時如果大部分人對某一個知識點討論的比較熱烈時,那一定是作者要麼講錯了,要麼沒講清楚。

這點確實是要把自己作為一個讀者的角度來看,不然很容易出現之前的一些誤解。

在這之外呢,我覺得對於執行緒池把這兩篇都看完同時也理解後對於大家理解執行緒池,利用執行緒池完成工作也是有很大好處的。

如果有在面試中加分的記得回來點贊、分享啊。

你的點贊與分享是對我最大的支援

執行緒池中你不容錯過的一些細節

相關文章