多執行緒--執行緒池的正確開啟方式
概述
執行緒可認為是作業系統可排程的最小的程式執行序列,一般作為程序的組成部分,同一程序中多個執行緒可共享該程序的資源(如記憶體等)。JVM執行緒跟核心輕量級程序有一對一的對映關係,所以JVM中的執行緒是很寶貴的。
一般在工程上多執行緒的實現是基於執行緒池的。因為相比自己建立執行緒,多執行緒具有以下優點
- 執行緒是稀缺資源,使用執行緒池可以減少建立和銷燬執行緒的次數,每個工作執行緒都可以重複使用。
- 可以根據系統的承受能力,調整執行緒池中工作執行緒的數量,防止因為消耗過多記憶體導致伺服器崩潰。
Executors存在什麼問題
看阿里巴巴開發手冊併發程式設計這塊有一條:執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式。
Executors為什麼存在缺陷
1. 執行緒池工作原理
當一個任務通過execute(Runnable)
方法欲新增到執行緒池時:
- 如果此時執行緒池中的數量小於
corePoolSize
,即使執行緒池中的執行緒都處於空閒狀態,也要建立新的執行緒來處理被新增的任務。 - 如果此時執行緒池中的數量等於
corePoolSize
,但是緩衝佇列workQueue
未滿,那麼任務被放入緩衝佇列。 - 如果此時執行緒池中的數量大於
corePoolSize
,緩衝佇列workQueue
滿,並且執行緒池中的數量小於maximumPoolSize
,建新的執行緒來處理被新增的任務。 - 那麼通過
handler
所指定的策略來處理此任務。也就是:處理任務的優先順序為:核心執行緒corePoolSize
workQueue
、最大執行緒maximumPoolSize
,如果三者都滿了,使用handler
處理被拒絕的任務。 - 當執行緒池中的執行緒數量大於
corePoolSize
時,如果某執行緒空閒時間超過keepAliveTime
,執行緒將被終止。這樣,執行緒池可以動態的調整池中的執行緒數。
2.newFixedThreadPool分析
Java中的BlockingQueue
主要有兩種實現,分別是ArrayBlockingQueue
和 LinkedBlockingQueue
。
ArrayBlockingQueue
是一個用陣列實現的有界阻塞佇列,必須設定容量。
LinkedBlockingQueue
Integer.MAX_VALUE
。
這裡的問題就出在:不設定的話,將是一個無邊界的阻塞佇列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設定LinkedBlockingQueue
的容量的話,其預設容量將會是Integer.MAX_VALUE
。
而newFixedThreadPool
中建立LinkedBlockingQueue
時,並未指定容量。此時,LinkedBlockingQueue
就是一個無邊界佇列,對於一個無邊界佇列來說,是可以不斷的向佇列中加入任務的,這種情況下就有可能因為任務過多而導致記憶體溢位問題。
3. newCachedThreadPool分析
結合上述流程圖,核心執行緒數=0,最大執行緒無限大,由於SynchronousQueue是一個快取值為1的阻塞佇列。當有大量任務請求時,執行緒池會建立大量執行緒,造成OOM。
執行緒池引數詳解
1. 構造方法
/**
* @param corePoolSize 核心執行緒數
* @param maximumPoolSize 最大執行緒數
* @param keepAliveTime 執行緒所允許的空閒時間
* @param unit 執行緒所允許的空閒時間的單位
* @param workQueue 執行緒池所使用的緩衝佇列
* @param handler 執行緒池對拒絕任務的處理策略
*/
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
2. 執行緒池拒絕策略
RejectedExecutionHandler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。。以下是JDK1.5提供的四種策略。
- AbortPolicy:直接丟擲異常
- CallerRunsPolicy:只用呼叫者所線上程來執行任務。
- DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
- DiscardPolicy:不處理,丟棄掉。
- 當然也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化不能處理的任務。
執行緒池正確開啟方式
1. 建立執行緒池
避免使用Executors建立執行緒池,主要是避免使用其中的預設實現,那麼我們可以自己直接呼叫ThreadPoolExecutor
的建構函式來自己建立執行緒池。在建立的同時,給BlockQueue
指定容量就可以了。
ThreadPoolExecutor executorService = new ThreadPoolExecutor(8,
16,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(10));
2. 向執行緒池提交任務
我們可以使用execute提交的任務,但是execute方法沒有返回值,所以無法判斷任務知否被執行緒池執行成功。通過以下程式碼可知execute方法輸入的任務是一個Runnable類的例項。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("執行緒池無返回結果");
}
});複製ErrorCopied
我們也可以使用submit 方法來提交任務,它會返回一個future,那麼我們可以通過這個future來判斷任務是否執行成功,通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60));
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "ok";
}
});
System.out.println("執行緒池返回結果:" + future.get());
3. 關閉執行緒池
shutdown關閉執行緒池
方法定義:public void shutdown()
(1)執行緒池的狀態變成SHUTDOWN狀態,此時不能再往執行緒池中新增新的任務,否則會丟擲RejectedExecutionException異常。
(2)執行緒池不會立刻退出,直到新增到執行緒池中的任務都已經處理完成,才會退出。
注意這個函式不會等待提交的任務執行完成,要想等待全部任務完成,可以呼叫:
public boolean awaitTermination(longtimeout, TimeUnit unit)
shutdownNow關閉執行緒池並中斷任務
方法定義:public List shutdownNow()
(1)執行緒池的狀態立刻變成STOP狀態,此時不能再往執行緒池中新增新的任務。
(2)終止等待執行的執行緒,並返回它們的列表;
(3)試圖停止所有正在執行的執行緒,試圖終止的方法是呼叫Thread.interrupt(),但是大家知道,如果執行緒中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的執行緒的。所以,ShutdownNow()並不代表執行緒池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。
4. 如何配置執行緒池大小
CPU密集型任務
該任務需要大量的運算,並且沒有阻塞,CPU一直全速執行,CPU密集任務只有在真正的多核CPU上才可能通過多執行緒加速 CPU密集型任務配置儘可能少的執行緒數量:
CPU核數+1個執行緒的執行緒池。
例如: CPU 16核,記憶體32G。執行緒數=16
IO密集型任務
IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如CPU核數*2
某大廠設定策略:IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數:
CPU核數/(1-阻塞係數)
例如: CPU 16核, 阻塞係數 0.9 ------------->16/(1-0.9) = 160 個執行緒數。
此時非阻塞執行緒=16
寫在最後
這篇文章是對執行緒池使用的簡單分析,為了更好的學習程式設計,後續會從原始碼的角度分析執行緒池的執行原理,上述文章如有錯漏,還望大家不吝賜教。
公眾號 【當我遇上你】