線程池中你不容錯過的一些細節

背景

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

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

這實際上是一類共性問題,我認爲主要仍是兩個緣由:異步

  • 我本身確實也沒講清楚,以前畫的那張圖還須要再完善,有些誤導。
  • 第二仍是你們對線程池的理解不夠深入,好比今天要探討的內容。

線程池的工做原理

首先仍是來複習下線程池的基本原理。工具

我認爲線程池它就是一個調度任務的工具。操作系統

衆所周知在初始化線程池會給定線程池的大小,假設如今咱們有 1000 個線程任務須要運行,而線程池的大小爲 10~20,在真正運行任務的過程當中他確定不會建立這1000個線程同時運行,而是充分利用線程池裏這 10~20 個線程來調度這1000個任務。線程

而這裏的 10~20 個線程最後會由線程池封裝爲 ThreadPoolExecutor.Worker 對象,而這個 Worker 是實現了 Runnable 接口的,因此他本身自己就是一個線程。code

深刻分析

這裏咱們來作一個模擬,建立了一個核心線程、最大線程數、阻塞隊列都爲2的線程池。cdn

這裏假設線程池已經完成了預熱,也就是線程池內部已經建立好了兩個線程 Worker對象

當咱們往一個線程池丟一個任務會發生什麼事呢?blog

  • 第一步是生產者,也就是任務提供者他執行了一個 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,不知道是你們都以爲這是基操仍是沒人仔細考慮過。

總結

針對以前線上事故的總結上次已經寫得差很少了,感興趣的能夠翻回去看看。

此次呢可能更可能是我本身的總結,好比寫一篇技術博客時若是大部分人對某一個知識點討論的比較熱烈時,那必定是做者要麼講錯了,要麼沒講清楚。

這點確實是要把本身做爲一個讀者的角度來看,否則很容易出現以前的一些誤解。

在這以外呢,我以爲對於線程池把這兩篇都看完同時也理解後對於你們理解線程池,利用線程池完成工做也是有很大好處的。

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

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

相關文章
相關標籤/搜索