java執行緒池建立方式
當前環境
- jdk == 1.8
Executors 使用的隱患
先來看一段程式碼,我們要建立一個固定執行緒池,假設固定執行緒數是4。程式碼如下:
Executors
是JAVA併發包中提供的,用來快速建立不同型別的執行緒池。
是不是很簡單,建立執行緒池只需一行程式碼。對於一些個人專案或臨時性的專案,這樣寫確實沒什麼問題,而且開發速度很快。但在一些大型專案中,這種做法一般是禁止的。
WHY???
因為用Executors
建立的執行緒池存在效能隱患,我們看一下原始碼就知道,用Executors
建立執行緒池時,使用的佇列是new LinkedBlockingQueue<Runnable>()
,這是一個無邊界佇列,如果不斷的往裡加任務時,最終會導致記憶體問題,也就是說在專案中由於使用了無邊界佇列,導致的記憶體佔用的不可控性。下圖是不斷新增執行緒任務導致老年代被佔滿的情況:
當然,除了記憶體問題,它還存在一些其他的問題,在下面對執行緒池引數的介紹中會具體說明。
執行緒池的正確建立方式
其實,問題很好解決。提供的簡便方式有侷限性,那我們自己new一個ThreadPoolExecutor
,無非多寫幾行程式碼而已。
關於ThreadPoolExecutor
的具體程式碼如下:
引數說明:
- corePoolSize:核心執行緒數;
- maximumPoolSize:最大執行緒數,即執行緒池中允許存在的最大執行緒數;
- keepAliveTime:執行緒存活時間,對於超過核心執行緒數的執行緒,當執行緒處理空閒狀態下,且維持時間達到keepAliveTime時,執行緒將被銷燬;
- unit:keepAliveTime的時間單位
- workQueue:工作佇列,用於存在待執行的執行緒任務;
- threadFactory:建立執行緒的工廠,用於標記區分不同執行緒池所創建出來的執行緒;
- handler:當到達執行緒數上限或工作佇列已滿時的拒絕處理邏輯;
具體程式碼
- 自定義threadFactory。除了可以自定義建立的執行緒名稱,方便問題排查,在
newThread(Runnable r)
建立執行緒的方法中,還可以進行定製化設定,如為執行緒設定特定上下文等。
- 自定義RejectedExecutionHandler。記錄異常資訊,選擇不同處理邏輯,有交由當前執行緒執行任務,有直接丟擲異常,再或者等待後繼續新增任務等。
- 建立自定義執行緒池
執行緒池內在處理邏輯
我們通過一些例子,來觀察一下其內部的處理邏輯。基於上述具體程式碼,我們已經建立了一個核心執行緒數4,最大執行緒數8,執行緒存活時間10s,工作佇列最大容量為10的一個執行緒池。
初始化執行緒池:未新增執行緒任務
- 這時,執行緒池中***不會建立任何執行緒***,存活執行緒為0,工作佇列為0.
未達核心執行緒數:新增4個執行緒任務
- 由於當前存活執行緒數 <= 核心執行緒數,所以會***建立新的執行緒***。即存活執行緒為4,工作佇列為0.
核心執行緒數已滿:新增第5個執行緒任務
- 若當前執行緒池中存在空閒執行緒,則交由該執行緒處理。即存活執行緒為4,工作佇列為0.
- 若當前所有執行緒處理執行狀態,加入工作佇列。即存活執行緒為4,工作佇列為1.(注意:此時工作佇列中的任務不會被執行,直到有執行緒空閒後,才能被處理)
工作佇列未滿:假設新增的任務都是耗時操作(短時間不會結束),再新增9個耗時任務
- 即存活執行緒為4,工作佇列為10.
工作佇列已滿 & 未達最大執行緒數:再新增4個任務
- 當工作佇列已滿,且不存在空閒執行緒,此時會***建立額外執行緒***來處理當前任務。此時存活執行緒為8,工作佇列為10.
工作佇列已滿 & 且最大執行緒數已滿:再新增1個任務
- 觸發RejectedExecutionHandler,將當前任務交由自己設定的執行控制代碼進行處理。此時存活執行緒為8,工作佇列為10.
當任務執行完後,沒有新增的任務,臨時擴充的執行緒(大於核心執行緒數的)將在10s(keepAliveTime)後被銷燬。
總結
最後,我們在使用執行緒池的時候,需要根據使用場景來自行選擇。通過corePoolSize和maximumPoolSize的搭配,存活時間的選擇,以及改變佇列的實現方式,如:選擇延遲佇列,來實現定時任務的功能。併發包Executors
中提供的一些方法確實好用,但我們仍需有保留地去使用,這樣在專案中就不會挖太多的坑。
擴充套件
對於一些耗時的IO任務,盲目選擇執行緒池往往不是最佳方案。通過非同步+單執行緒輪詢,上層再配合上一個固定的執行緒池,效果可能更好。類似與Reactor模型中selector輪詢處理。
文章轉自:http://www.iteye.com/news/32893#comments