Java執行緒池的7個引數說明及配置參考
在Java開發中,經常會使用到多執行緒來處理一些資料聚合的場景或者是多工的場景,嚐嚐使用到執行緒池技術,這邊文章將詳細的講解在建立執行緒池時的7個引數的解釋;
多執行緒使用的得當的話,可以很好的提高程式碼的執行效率,提高任務的處理效能;如果使用的不當,相反會造成各種問題並且不太好排查;
如下圖所示,在使用多執行緒場景非同步訊息,可以有效的降低資料聚合的時間,從何提高使用者體驗;
在阿里巴巴Java開發手冊中明確禁止執行緒池使用Executors去建立,而是通過ThreadPoolExecutor的方式進行建立,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,從而可以避免資源耗盡的風險;
建構函式
通過原始碼可以看到ThreadPoolExecutor的建構函式如下
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
構造引數
通過上述建構函式可以看到,在建立一個執行緒池的時候,一共有7個引數,這7個引數的意思如下:
- corePoolSize:表示當前執行緒池的核心執行緒數大小,即最小執行緒數(初始化執行緒數),執行緒池會維護當前資料的執行緒線上程池中,即使這些執行緒一直處於閒置狀態,也不會被銷燬;
- maximumPoolSize:表示執行緒池中允許的最大執行緒數;後文中會詳細講解
- keepAliveTime :表示空閒執行緒的存活時間,當執行緒池中的執行緒數量大於核心執行緒數且執行緒處於空閒狀態,那麼在指定時間後,這個空閒執行緒將會被銷燬,從而逐漸恢復到穩定的核心執行緒數數量;
- unit:當前unit表示的是keepAliveTime存活時間的計量單位,通常使用TimeUnit.SECONDS秒級;
- workQueue:任務工作佇列;後文會結合maximumPoolSize一塊來講
- threadFactory:執行緒工廠,用於建立新執行緒以及為執行緒起名字等
- handler:拒絕策略,即當任務過多無法及時處理時所需採取的策略;
在上面有提到一個最大執行緒數,當一個新任務來到執行緒池時,首先會先將當前任務放到workQueue(任務工作佇列)裡面,然後交由排程任務從佇列中取出任務,交由執行緒池中的執行緒進行處理;
如果工作佇列滿了,則會通過執行緒工廠建立一個新的執行緒到執行緒池,然後從工作任務中取出一個任務交由新執行緒處理,而剛提交的任務則放入到工作任務佇列中;
當工作佇列中的任務也達到最大限制並且執行緒池中的執行緒數量達到最大執行緒數,這時候則會觸發執行緒池的拒絕策略;
工作任務佇列分類
在jdk中,一共提供了4種工作任務佇列,分別如下所示:
1、ArrayBlockingQueue:通過名字我們可以推測出,當前佇列是基於Array陣列實現的,陣列的特性是初始化時需要指定陣列的大小,也就是指定了儲存的工作任務上限;ArrayBlockingQueue是一個基於資料的有界的阻塞佇列,新加入的任務放到佇列的隊尾,等待被排程;如果佇列已經滿了,則會建立新執行緒;如果執行緒池數量也滿了,則會執行拒絕策略;
2、LinkedBlockingQuene:通過名字我們可以推測出,當前佇列是基於Linked連結串列來實現的,連結串列的特性是沒有初始容量,也就意味著這個佇列是無界的,最大容量可以達到Integer.MAX。也由於LinkedBlockingQuene的無界特性,當有新的任務進來,會一直儲存在當前佇列中,等待排程任務來進行排程;在此場景下,引數maximumPoolSize是無效的;LinkedBlockingQuene可能會帶來資源耗盡的問題;
3、SynchronousQuene:同步佇列,一個不快取任務的阻塞佇列,生產者放入一個任務必須等到消費者取出任務,直接被排程任務排程執行當前任務;如果沒有空閒的可用執行緒,則直接建立新的執行緒進行處理,當執行緒池數量達到maximumPoolSize時,則觸發拒絕策略;
4、PriorityBlockingQueue:優先考慮無界阻塞佇列,優先順序可以通過Comparator來實現;
執行緒池拒絕策略
在前面提到過,當工作任務佇列達到最大值並且執行緒池的容量也達到了最大執行緒數時,當有新的任務進來時,則會觸發拒絕策略;在JDK中一共提供了4種拒絕策略,分別如下:
1、CallerRunsPolicy
該策略下,在呼叫者執行緒中直接執行被拒絕任務的run方法,除非執行緒池已經shutdown,則直接拋棄任務。
2、AbortPolicy
該策略下,直接丟棄任務,並丟擲RejectedExecutionException異常。
3、DiscardPolicy
該策略下,直接丟棄任務,什麼都不做。
4、DiscardOldestPolicy
該策略下,拋棄進入佇列最早的那個任務,然後嘗試把這次拒絕的任務放入佇列
建立執行緒池示例程式碼
在實際應用場景中,可能大部分執行緒池的建立如下所示
/**
* 獲取當前執行機器的可用CPU核心數
*/
private final static int POLLER_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
@Bean
public ExecutorService executorService(){
return new ThreadPoolExecutor(
POLLER_THREAD_COUNT,
POLLER_THREAD_COUNT * 8,
10L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(64),
new ThreadFactoryBuilder().setNameFormat("AppName_FutureTask-%d").setDaemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy());
}
通過執行程式碼可以看到如下
在上述程式碼中,通過函式Runtime.getRuntime().availableProcessors()獲取到當前執行環境的可用處理器數量,然後基於當前可用核心數數量來決定最大執行緒數;
我們可以根據實際的業務場景或者是硬體指標來決定最大核心執行緒數,也就是所謂的 CPU密集型 與 IO密集型;
另外提一點,我們在使用執行緒時,很多時候為執行緒配置一下環境資訊,通常會使用 ThreadLocal 用來儲存資料;但是在多執行緒執行緒下,如果子執行緒需要使用主執行緒的資料時,使用ThreadLocal 是無法實現資料訪問的;
這個時候可以採用InheritableThreadLocal來實現,InheritableThreadLocal可以實現父執行緒傳遞本地變數到子執行緒;不過在實際應用場景中,好像也還是有一點缺陷,我看我們這邊專案中最後逐漸迭代到使用阿里的TransmittableThreadLocal,即阿里的TTL,引入依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.1</version>
</dependency>
艾歐尼亞,昂揚不滅,為了更美好的明天而戰(#^.^#)