帶着問題
- 阿里Java代碼規范為什么不允許使用Executors快速創建線程池?
- 下面的代碼輸出是什么?
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, //corePoolSize
100, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(100));//workQueue
for (int i = 0; i < 5; i++) {
final int taskIndex = i;
executor.execute(() -> {
System.out.println(taskIndex);
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
A) 0 1 2 3 4 5
B) 0~5 順序不一致輸出5行
C) 0
基礎
什么是線程池?
線程池可以通過池看出來是一個資源集,任何池的作用都大同小異,主要是用來減少資源創建、初始化的系統開銷。
創建線程很“貴”嗎?
是的。創建線程的代價是昂貴的。
我們都知道系統中的每個進程有自己獨立的內存空間,而被稱為輕量級進程的線程也是需要的。
在JVM中默認一個線程需要使用256k~1M(取決於32位還是64位操作系統)的內存。(具體的數組我們不深究,因為隨着JVM版本的變化這個默認值隨時可能發生變更,我們只需要知道線程是需要占用內存的)
除了內存還有更多嗎?
許多文章會將上下文切換、CPU調度列入其中,這邊不將線程調度列入是因為睡眠中的線程不會被調度(OS控制),如果不是睡眠中的線程那么是一定需要被調度的。
但在JVM中除了創建時的內存消耗,還會給GC帶來壓力,如果頻繁創建線程那么相對的GC的時候也需要回收對應的線程。
線程池的機制?
可以看到線程池是一種重復利用線程的技術,線程池的主要機制就是保留一定的線程數在沒有事情做的時候使之睡眠,當有活干的時候拿一個線程去運行。
這些牽扯到線程池實現的具體策略。
還有哪些常見的池?
- 線程池
- 連接池(數據庫連接、TCP連接等)
- BufferPool
- ......
Java中的線程池
UML圖(Java 8)
可以看到真正的實現類有
- ThreadPoolExecutor (1.5)
- ForkJoinPool (1.7)
- ScheduledThreadPoolExecutor (1.5)
今天我們主要談談 ThreadPoolExecutor
也是使用率較高的一個實現。
Executors提供的工廠方法
-
newCachedThreadPool (ThreadPoolExecutor)
創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
-
newFixedThreadPool (ThreadPoolExecutor)
創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。
-
newSingleThreadExecutor (ThreadPoolExecutor)
創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
-
newScheduledThreadPool (ScheduledThreadPoolExecutor)
創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求。
-
newSingleThreadScheduledExecutor (ScheduledThreadPoolExecutor)
創建一個單線程用於定時以及周期性執行任務的需求。
-
newWorkStealingPool (1.8 ForkJoinPool)
創建一個工作竊取
可以看到各種不同的工廠方法中使用的線程池實現類最終只有3個,對應關系如下:
工廠方法 | 實現類 |
---|---|
newCachedThreadPool | ThreadPoolExecutor |
newFixedThreadPool | ThreadPoolExecutor |
newSingleThreadExecutor | ThreadPoolExecutor |
newScheduledThreadPool | ScheduledThreadPoolExecutor |
newSingleThreadScheduledExecutor | ScheduledThreadPoolExecutor |
newWorkStealingPool | ForkJoinPool |
ThreadPoolExecutor
首先我們看下 ThreadPoolExecutor
的完全構造函數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize
核心池大小,除非設置了
allowCoreThreadTimeOut
否則哪怕線程超過空閑時間,池中也要最少要保留這個數目的線程。需要注意的是,corePoolSize所需的線程並不是立即創建的,需要在提交任務之后進行創建,所以如果有大量的緩存線程數可以先提交一個空任務讓線程池將線程先創建出來,從而提升后續的執行效率。
-
maximumPoolSize
允許的最大線程數。
-
keepAliveTime
空閑線程空閑存活時間,核心線程需要
allowCoreThreadTimeOut
為true才會退出。 -
unit
與
keepAliveTime
配合,設置keepAliveTime
的單位,如:毫秒、秒。 -
workQueue
線程池中的任務隊列。上面提到線程池的主要作用是復用線程來處理任務,所以我們需要一個隊列來存放需要執行的任務,在使用池中的線程來處理這些任務,所以我們需要一個任務隊列。
-
threadFactory
當線程池判斷需要新的線程時通過線程工程創建線程。
-
handler
執行被阻止時的處理程序,線程池無法處理。這個與任務隊列相關,比如隊列中可以指定隊列大小,如果超過了這個大小該怎么辦呢?JDK已經為我們考慮到了,並提供了4個默認實現。
下列是JDK中默認攜帶的策略:
- AbortPolicy (默認)
拋出
RejectedExecutionException
異常。- CallerRunsPolicy
調用當前線程池所在的線程去執行。
- DiscardPolicy
直接丟棄當前任務。
- DiscardOldestPolicy
將最舊的任務丟棄,將當前任務添加到隊列。
容易混淆的參數:corePoolSize maximumPoolSize workQueue
任務隊列、核心線程數、最大線程數的邏輯關系
- 當線程數小於核心線程數時,創建線程。
- 當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
- 當線程數大於等於核心線程數,且任務隊列已滿
- 若線程數小於最大線程數,創建線程
- 若線程數等於最大線程數,調用拒絕執行處理程序(默認效果為:拋出異常,拒絕任務)
那么這三個參數推薦如何設置,有最優值嗎?
由於java對於協程的支持不友好,所以會大量依賴於線程池和線程。
從而這個值沒有最優推薦,需要根據業務需求情況來進行設置。
不同的需求類型可以創建多個不同的線程池來執行。
問題1:阿里開發規范為什么不允許Executors快速創建線程池?
可以看到原因很簡單
- newSingleThreadExecutor
- newFixedThreadPool
在 workQueue
參數直接 使用了 new LinkedBlockingQueue<Runnable>()
理論上可以無限添加任務到線程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>();
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
如果提交到線程池的任務由問題,比如 sleep 永久,會造成內存泄漏,最終導致OOM。
同時 阿里還推薦自定義 threadFactory
設置線程名稱便於以后排查問題。
問題2:下面的代碼輸出是什么?
應該選C。
雖然最大線程數有100但核心線程數為1,任務隊列由100。
滿足了 '當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。' 這個條件。
所以后續添加的任務都會被堵塞。
最后
關於 ThreadPoolExecutor 的邏輯在實際使用的時候會有點奇怪,因為線程池中的線程並沒有超過最大線程數,有沒有一種可能當任務被堵塞很久的時候創建新的線程池來處理呢?
這邊推薦大家使用 newWorkStealingPool,也就是ForkJoinPool。采取了工作竊取的模式。
后續會跟大家一起聊聊 ForkJoinPool。