java線程池工作原理和實現原理


為什么要使用線程池

平時討論多線程處理,大佬們必定會說使用線程池,那為什么要使用線程池?其實,這個問題可以反過來思考一下,不使用線程池會怎么樣?當需要多線程並發執行任務時,只能不斷的通過new Thread創建線程,每創建一個線程都需要在堆上分配內存空間,同時需要分配虛擬機棧、本地方法棧、程序計數器等線程私有的內存空間,當這個線程對象被可達性分析算法標記為不可用時被GC回收,這樣頻繁的創建和回收需要大量的額外開銷。再者說,JVM的內存資源是有限的,如果系統中大量的創建線程對象,JVM很可能直接拋出OutOfMemoryError異常,還有大量的線程去競爭CPU會產生其他的性能開銷,更多的線程反而會降低性能,所以必須要限制線程數。

既然不使用線程池有那么多問題,我們來看一下使用線程池有哪些好處:

  • 使用線程池可以復用池中的線程,不需要每次都創建新線程,減少創建和銷毀線程的開銷;
  • 同時,線程池具有隊列緩沖策略、拒絕機制和動態管理線程個數,特定的線程池還具有定時執行、周期執行功能,比較重要的一點是線程池可實現線程環境的隔離,例如分別定義支付功能相關線程池和優惠券功能相關線程池,當其中一個運行有問題時不會影響另一個。

如何構造一個線程池對象

本文內容我們只聊線程池ThreadPoolExecutor,查看它的源碼會發現它繼承了AbstractExecutorService抽象類,而AbstractExecutorService實現了ExecutorService接口,ExecutorService繼承了Executor接口,所以ThreadPoolExecutor間接實現了ExecutorService接口和Executor接口,它們的關系圖如下。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

一般我們使用的execute方法是在Executor接口中定義的,而submit方法是在ExecutorService接口中定義的,所以當我們創建一個Executor類型變量引用ThreadPoolExecutor對象實例時可以使用execute方法提交任務,當我們創建一個ExecutorService類型變量時可以使用submit方法,當然我們可以直接創建ThreadPoolExecutor類型變量使用execute方法或submit方法。

ThreadPoolExecutor定義了七大核心屬性,這些屬性是線程池實現的基石。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

corePoolSize(int):核心線程數量。默認情況下,在創建了線程池后,線程池中的線程數為0,當有任務來之后,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize后,就會把到達的任務放到任務隊列當中。線程池將長期保證這些線程處於存活狀態,即使線程已經處於閑置狀態。除非配置了allowCoreThreadTimeOut=true,核心線程數的線程也將不再保證長期存活於線程池內,在空閑時間超過keepAliveTime后被銷毀。

workQueue:阻塞隊列,存放等待執行的任務,線程從workQueue中取任務,若無任務將阻塞等待。當線程池中線程數量達到corePoolSize后,就會把新任務放到該隊列當中。JDK提供了四個可直接使用的隊列實現,分別是:基於數組的有界隊列ArrayBlockingQueue、基於鏈表的無界隊列LinkedBlockingQueue、只有一個元素的同步隊列SynchronousQueue、優先級隊列PriorityBlockingQueue。在實際使用時一定要設置隊列長度。

maximumPoolSize(int):線程池內的最大線程數量,線程池內維護的線程不得超過該數量,大於核心線程數量小於最大線程數量的線程將在空閑時間超過keepAliveTime后被銷毀。當阻塞隊列存滿后,將會創建新線程執行任務,線程的數量不會大於maximumPoolSize。

keepAliveTime(long):線程存活時間,若線程數超過了corePoolSize,線程閑置時間超過了存活時間,該線程將被銷毀。除非配置了allowCoreThreadTimeOut=true,核心線程數的線程也將不再保證長期存活於線程池內,在空閑時間超過keepAliveTime后被銷毀。

TimeUnit unit:線程存活時間的單位,例如TimeUnit.SECONDS表示秒。

RejectedExecutionHandler:拒絕策略,當任務隊列存滿並且線程池個數達到maximunPoolSize后采取的策略。ThreadPoolExecutor中提供了四種拒絕策略,分別是:拋RejectedExecutionException異常的AbortPolicy(如果不指定的默認策略)、使用調用者所在線程來運行任務CallerRunsPolicy、丟棄一個等待執行的任務,然后嘗試執行當前任務DiscardOldestPolicy、不動聲色的丟棄並且不拋異常DiscardPolicy。項目中如果為了更多的用戶體驗,可以自定義拒絕策略。

threadFactory:創建線程的工廠,雖說JDK提供了線程工廠的默認實現DefaultThreadFactory,但還是建議自定義實現最好,這樣可以自定義線程創建的過程,例如線程分組、自定義線程名稱等。

 

一般我們使用類的構造方法創建它的對象,ThreadPoolExecutor提供了四個構造方法。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

可以看到前三個方法最終都調用了最后一個、參數列表最長的那個方法,在這個方法中給七個屬性賦值。創建線程池對象,強烈建議通過使用ThreadPoolExecutor的構造方法創建,不要使用Executors,至於建議的理由上文中也有說過,這里再引用阿里《Java開發手冊》中的一段描述。

【強制】線程池不允許使用Executors創建,建議通過ThreadPoolExecutor的方式創建,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

說明:Executors返回的線程池對象的弊端如下:

1.FixedThreadPool和SingleThreadPool:

允許的請求隊列長度為Integet.MAX_VALUE,可能會堆積大量的請求從而導致OOM;

2.CachedThreadPool:

允許創建線程數量為Integet.MAX_VALUE,可能會創建大量的線程,從而導致OOM.

了解了線程池ThreadPoolExecutor的基本構造,接下來編寫一段代碼看看如何使用,樣例代碼中的參數僅為了配合原理解說使用。

 

 

線程池工作原理

關於線程池的工作原理,我用下面的7幅圖來展示。

1.通過execute方法提交任務時,當線程池中的線程數小於corePoolSize時,新提交的任務將通過創建一個新線程來執行,即使此時線程池中存在空閑線程。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

2.通過execute方法提交任務時,當線程池中線程數量達到corePoolSize時,新提交的任務將被放入workQueue中,等待線程池中線程調度執行。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

3.通過execute方法提交任務時,當workQueue已存滿,且maximumPoolSize大於corePoolSize時,新提交的任務將通過創建新線程執行。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

4.當線程池中的線程執行完任務空閑時,會嘗試從workQueue中取頭結點任務執行。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

5.通過execute方法提交任務,當線程池中線程數達到maxmumPoolSize,並且workQueue也存滿時,新提交的任務由RejectedExecutionHandler執行拒絕操作。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

6.當線程池中線程數超過corePoolSize,並且未配置allowCoreThreadTimeOut=true,空閑時間超過keepAliveTime的線程會被銷毀,保持線程池中線程數為corePoolSize。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

注意:上圖表達的是銷毀空閑線程,保持線程數為corePoolSize,不是銷毀corePoolSize中的線程。

7.當設置allowCoreThreadTimeOut=true時,任何空閑時間超過keepAliveTime的線程都會被銷毀。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

線程池底層實現原理

查看ThreadPoolExecutor的源碼,發現ThreadPoolExecutor的實現還是比較復雜的,下面簡單介紹幾個重要的全局常量和方法。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

ctl用於表示線程池的狀態和線程數,在ThreadPoolExecutor中使用32位二進制數來表示線程池的狀態和線程池中線程數量,其中前3位表示線程池狀態,后29位表示線程池中線程數。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))初始化線程池狀態為RUNNING、線程池數量為0。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

COUNT_BITS值等於Integer.SIZE - 3,在源碼中Integer.SIZE是32,所以COUNT_BITS=29。CAPACITY表示線程池允許的最大線程數,轉算后的結果如下。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED分別表示線程池的不同狀態,轉算后的結果如下。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

線程池處在不同的狀態時,它的處理能力是不同的。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

線程池不同狀態之間的轉換時機及轉換關系如下圖。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

runStateOf獲取ctl高三位,也就是線程池的狀態。workerCountOf獲取ctl低29位,也就是線程池中線程數。ctlOf計算ctlOf新值,也就是線程池狀態和線程池個數。

你可能會疑問“為什么要介紹上面這些?”,這是因為接下來的源碼分析會用到這些基礎的知識點。一般,我們使用ThreadPoolExecutor的execute方法提交任務,所以從execute的源碼入手。

 

 

為了更輕松的理解上圖中的源碼,我又畫了一個流程圖。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

到這里線程池的基本實現原理已經很清晰了,接下來我們重點分析一下線程池中線程是如何執行任務、如何復用線程和線程空閑時間超限如何判斷的。還是從execute方法入手,我們直接看它里面調用的addWorker方法,它實現了創建新線程執行任務。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

源碼中將線程和任務封裝到了Worker中,然后將Worker添加到HashSet集合中,添加成功后通過線程對象的start方法啟動線程執行任務,既然這樣那我們就來看看上圖代碼中的w = new Worker(firstTask)到底是如何執行的。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

Worker繼承了AbstractQueuedSynchronizer,並且實現了Runnable接口,看到這里很清楚了任務最終由Worker中的run方法執行,而run方法里調用了runWorker方法,所以重點還是runWorker方法。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

在runWorker方法中,使用循環,通過getTask方法,不斷從阻塞隊列中獲取任務執行,如果任務不為空則執行任務,這里實現了線程的復用,不斷的獲取任務執行,不用重新創建線程;隊列中獲取的任務為null,則將Worker從HashSet集合中清除,注意這個清除就是空閑線程的回收。那getTask何時返回null?接着看getTask源碼。

我畫了25張圖展示線程池工作原理和實現原理,建議先收藏再閱讀
 
 

到這里,線程池中線程是如何執行任務、如何復用線程,以及線程空閑時間超限如何判斷都已經清楚了。

實踐建議

使用構造方法創建線程池

細心的朋友會發現,全文竟沒有介紹Executors,這個創建線程池的輔助工具類。是的,我強烈不推薦使用它,因為Executors中的newFixedThreadPool和newSingleThreadExecutor方法創建的線程池中,阻塞隊列LinkedBlockingQueue的長度是Integer.MAX_VALUE,可能會堆積大量的任務,從而導致 OOM;而newCachedThreadPool方法創建的線程池中最大線程數是Integer.MAX_VALUE,會創建大量的線程,從而導致OOM。如果創建線程池,通過ThreadPoolExecutor的構造方法創建,這樣使用這個線程池的人會更加明確線程池的各個參數的設置及運行方式,提前避免隱藏問題的發生。

使用自定義線程工廠

為什么要這么做呢?是因為,當項目規模逐漸擴展,各系統中線程池也不斷增多,當發生線程執行問題時,通過自定義線程工廠創建的線程設置有意義的線程名稱可快速追蹤異常原因,高效、快速的定位問題。

使用自定義拒絕策略

雖然,JDK給我們提供了一些默認的拒絕策略,但我們可以根據項目需求的需要,或者是用戶體驗的需要,定制拒絕策略,完成特殊需求。

線程池划分隔離

不同業務、執行效率不同的分不同線程池,避免因某些異常導致整個線程池利用率下降或直接不可用,進而影響整個系統或其它系統的正常運行。

小結

實際工作中,我們經常使用線程池,對這塊的要求不僅是常規的如何使用,原理我們也要清楚是怎么回事。同時,線程池工作原理和底層實現原理也是面試必問的考題,所以,這塊是一定要掌握的。


免責聲明!

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



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