摘要:第二還是大家對線程池的理解不夠深刻,比如今天要探討的內容。我認為線程池它就是一個調度任務的工具。而在線程池這個場景中卻恰好就是要利用它只是一個普通方法調用。
背景
上周分享了一篇《一個線程罷工的詭異事件》,最近也在公司內部分享了這個案例。
無獨有偶,在內部分享的時候也有小伙伴問了之前分享時所提出的一類問題:
這其實是一類共性問題,我認為主要還是兩個原因:
我自己確實也沒講清楚,之前畫的那張圖還需要再完善,有些誤導。
第二還是大家對線程池的理解不夠深刻,比如今天要探討的內容。
線程池的工作原理首先還是來復習下線程池的基本原理。
我認為線程池它就是一個調度任務的工具。
眾所周知在初始化線程池會給定線程池的大小,假設現在我們有 1000 個線程任務需要運行,而線程池的大小為 10~20,在真正運行任務的過程中他肯定不會創建這1000個線程同時運行,而是充分利用線程池里這 10~20 個線程來調度這1000個任務。
而這里的 10~20 個線程最后會由線程池封裝為 ThreadPoolExecutor.Worker 對象,而這個 Worker 是實現了 Runnable 接口的,所以他自己本身就是一個線程。
深入分析這里我們來做一個模擬,創建了一個核心線程、最大線程數、阻塞隊列都為2的線程池。
這里假設線程池已經完成了預熱,也就是線程池內部已經創建好了兩個線程 Worker。
當我們往一個線程池丟一個任務會發生什么事呢?
第一步是生產者,也就是任務提供者他執行了一個 execute() 方法,本質上就是往這個內部隊列里放了一個任務。
之前已經創建好了的 Worker 線程會執行一個 while 循環 ---> 不停的從這個內部隊列里獲取任務。(這一步是競爭的關系,都會搶著從隊列里獲取任務,由這個隊列內部實現了線程安全。)
獲取得到一個任務后,其實也就是拿到了一個 Runnable 對象(也就是 execute(Runnable task) 這里所提交的任務),接著執行這個 Runnable 的 run() 方法,而不是 start(),這點需要注意后文分析原因。
結合源碼來看:
從圖中其實就對應了剛才提到的二三兩步:
while 循環,從 getTask() 方法中一直不停的獲取任務。
拿到任務后,執行它的 run() 方法。
這樣一個線程就調度完畢,然后再次進入循環從隊列里取任務并不斷的進行調度。
再次解釋之前的問題接下來回顧一下我們上一篇文章所提到的,導致一個線程沒有運行的根本原因是:
在單個線程的線程池中一但拋出了未被捕獲的異常時,線程池會回收當前的線程并創建一個新的 Worker;
它也會一直不斷的從隊列里獲取任務來執行,但由于這是一個消費線程,根本沒有生產者往里邊丟任務,所以它會一直 waiting 在從隊列里獲取任務處,所以也就造成了線上的隊列沒有消費,業務線程池沒有執行的問題。
結合之前的那張圖來看:
這里大家問的最多的一個點是,為什么會沒有是根本沒有生產者往里邊丟任務,圖中不是明明畫的有一個 product 嘛?
這里確實是有些不太清楚,再次強調一次:
圖中的 product 是往內部隊列里寫消息的生產者,并不是往這個 Consumer 所在的線程池中寫任務的生產者。
因為即便 Consumer 是一個單線程的線程池,它依然具有一個常規線程池所具備的所有條件:
Worker 調度線程,也就是線程池運行的線程;雖然只有一個。
內部的阻塞隊列;雖然長度只有1。
再次結合圖來看:
所以之前提到的【沒有生產者往里邊丟任務】是指右圖放大后的那一塊,也就是內部隊列并沒有其他線程往里邊丟任務執行 execute() 方法。
而一旦發生未捕獲的異常后,Worker1 被回收,順帶的它所調度的線程 task1(這個task1 也就是在執行一個 while 循環消費左圖中的那個隊列) 也會被回收掉。
新創建的 Worker2 會取代 Worker1 繼續執行 while 循環從內部隊列里獲取任務,但此時這個隊列就一直會是空的,所以也就是處于 Waiting 狀態。
我覺得這波解釋應該還是講清楚了,歡迎還沒搞明白的朋友留言討論。為什是 run() 而不是 start()
問題搞清楚后來想想為什么線程池在調度的時候執行的是 Runnable 的 run() 方法,而不是 start() 方法呢?
我相信大部分沒有看過源碼的同學心中第一個印象就應該是執行的 start() 方法;
因為不管是學校老師,還是網上大牛講的都是只有執行了 start() 方法后操作系統才會給我們創建一個獨立的線程來運行,而 run() 方法只是一個普通的方法調用。
而在線程池這個場景中卻恰好就是要利用它只是一個普通方法調用。
回到我在文初中所提到的:我認為線程池它就是一個調度任務的工具。
假設這里是調用的 Runnable 的 start 方法,那會發生什么事情。
如果我們往一個核心、最大線程數為 2 的線程池里丟了 1000 個任務,那么它會額外的創建 1000 個線程,同時每個任務都是異步執行的,一下子就執行完畢了。
從而沒法做到由這兩個 Worker 線程來調度這 1000 個任務,而只有當做一個同步阻塞的 run() 方法調用時才能滿足這個要求。
這事也讓我發現一個奇特的現象:就是網上幾乎沒人講過為什么在線程池里是 run 而不是 start,不知道是大家都覺得這是基操還是沒人仔細考慮過。總結
針對之前線上事故的總結上次已經寫得差不多了,感興趣的可以翻回去看看。
這次呢可能更多是我自己的總結,比如寫一篇技術博客時如果大部分人對某一個知識點討論的比較熱烈時,那一定是作者要么講錯了,要么沒講清楚。
這點確實是要把自己作為一個讀者的角度來看,不然很容易出現之前的一些誤解。
在這之外呢,我覺得對于線程池把這兩篇都看完同時也理解后對于大家理解線程池,利用線程池完成工作也是有很大好處的。
如果有在面試中加分的記得回來點贊、分享啊。
你的點贊與分享是對我最大的支持
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73923.html
摘要:前言前段時間寫過一篇線程池沒你想的那么簡單,和大家一起擼了一個基本的線程池,具備線程池基本調度功能。線程池自動擴容縮容?;卣{以上就是線程池的構造函數以及接口的定義。所以我們在使用線程池時,其中的任務一定要做好異常處理。線程異常捕獲的重要性。 showImg(https://segmentfault.com/img/remote/1460000019403163?w=1904&h=108...
摘要:如何優雅的使用和理解線程池線程池中你不容錯過的一些細節由于篇幅限制,本次可能會分為上下兩篇。不接受新的任務,同時等待現有任務執行完畢后退出線程池。慎用方法關閉線程池,會導致任務丟失除非業務允許。前言 原以為線程池還挺簡單的(平時常用,也分析過原理),這次是想自己動手寫一個線程池來更加深入的了解它;但在動手寫的過程中落地到細節時發現并沒想的那么容易。結合源碼對比后確實不得不佩服 Doug Le...
摘要:如何優雅的使用和理解線程池線程池中你不容錯過的一些細節由于篇幅限制,本次可能會分為上下兩篇。不接受新的任務,同時等待現有任務執行完畢后退出線程池。慎用方法關閉線程池,會導致任務丟失除非業務允許。 showImg(https://segmentfault.com/img/remote/1460000019230693); 前言 原以為線程池還挺簡單的(平時常用,也分析過原理),這次是想自...
摘要:月日,國家會議中心,由主辦的合稱將強勢登陸北京這是首次來華,在這場三合一的開源技術盛會中,來自國內外的開發人員架構師系統管理員專家商業領袖等數千名專業人士將匯聚一堂。后被收購,梁勝出任云平臺首席技術官,也成為首位華人。 6月19-20日,國家會議中心,由The Linux Foundation主辦的LinuxCon + ContainerCon + CloudOpen (合稱LC3) ...
閱讀 3326·2021-11-16 11:45
閱讀 4402·2021-09-22 15:38
閱讀 2846·2021-09-22 15:26
閱讀 3355·2021-09-01 10:48
閱讀 849·2019-08-30 15:56
閱讀 723·2019-08-29 13:58
閱讀 1494·2019-08-28 18:00
閱讀 2168·2019-08-27 10:53