爲什麼使用ThreadPoolExecutor
在android開發中經常會使用多線程異步來處理相關任務,而如果用傳統的newThread來創建一個子線程進行處理,會造成一些嚴重的問題:
1:在任務衆多的情況下,系統要爲每一個任務創建一個線程,而任務執行完畢後會銷燬每一個線程,所以會造成線程頻繁地創建與銷燬。
2:多個線程頻繁地創建會佔用大量的資源,並且在資源競爭的時候就容易出現問題,同時這麼多的線程缺乏一個統一的管理,容易造成界面的卡頓。
3:多個線程頻繁地銷燬,會頻繁地調用GC機制,這會使性能降低,又非常耗時。
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
參數
corePoolSize=>
線程池裏的核心線程數量maximumPoolSize
=> 線程池裏允許有的最大線程數量keepAliveTime=>
空閒線程存活時間unit=>
keepAliveTime的時間單位,比如分鐘,小時等workQueue=> 緩衝
隊列threadFactory=>
線程工廠用來創建新的線程放入線程池handler=>
線程池拒絕任務的處理策略,比如拋出異常等策略
一開始我們看到這些參數內心肯定是拒絕的,看源碼也有些崩潰,那這些參數到底是些什麼意思呢?
或者給一組實際數據:
corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞隊列,隊列大小是4
handler:默認的策略,拋出來一個ThreadPoolRejectException
這些代表什麼呢?
參數可視化
我們把線程池比作一個花瓶
這個花瓶由 瓶口 、 瓶頸 、 瓶身 三個部分組成。
這三個部分分別對應着線程池的三個參數:maximumPoolSize, workQueue,corePoolSize。
改變corePoolSize
改變workQueue
線程池裏的線程,我用一個紅色小球表示,每來一個任務,就會生成一個小球:
而核心線程,也就是正在處理中的任務,則用灰色的虛線小球表示 (目前第一版動畫先這樣簡陋點吧......)
我們往線程池中增加任務
於是畫風就變成了這樣,“花瓶”有這麼幾個重要的參數:
- corePoolSize=> 瓶身的容量
- maximumPoolSize=> 瓶口的容量
- keepAliveTime=> 紅色小球的存活時間
- unit=> keepAliveTime的時間單位,比如分鐘,小時等
- workQueue=> 瓶頸,不同類型的瓶頸容量不同
- threadFactory=> 你投遞小球進花瓶的小手 (線程工廠)
- handler=> 線程池拒絕任務的處理策略,比如小球被排出瓶外
如果往這個花瓶裏面放入很多小球時(線程池執行任務);
瓶身 (corePoolSize) 裝不下了, 就會堆積到 瓶頸 (queue) 的位置;
瓶頸還是裝不下, 就會堆積到 瓶口 (maximumPoolSize);
直到最後小球從瓶口溢出。
還記得上面提到的那一組實際參數嗎,代表的花瓶大體上是如下圖這樣的:
那麼參數可視化到底有什麼實際意義呢?
阿里的規範
我們最開始 接觸ThreadPoolExcutor的時候,一般使用Executors去創建線程池,但是阿里開發手冊中對於 Java 線程池的使用規範:
一開始我並不知道爲什麼要去限制這樣使用,這四種線程池爲什麼會導致OOM,
我們看看這四種線程池的具體參數,然後再用花瓶動畫演示一下導致OOM的原因。
線程池FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
我們關心的參數如下
corePoolSize:nThreads
mamximumPoolSize:nThreads
workQueue:LinkedBlockingQueue
FixedThreadPool表示的花瓶就是下圖這樣子:
線程池SingleThreadPool:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
我們關心的參數如下
corePoolSize:1
mamximumPoolSize:1
workQueue:LinkedBlockingQueue
SingleThreadPool表示的花瓶就是下圖這樣子:
雖然兩個線程池的樣子沒什麼差異,但是這裏我們發現了一個問題:
爲什麼 FixedThreadPool 和 SingleThreadPool 的 corePoolSize和mamximumPoolSize 要設計成一樣的?
回答這個問題, 我們應該關注一下線程池的 workQueue 參數。
線程池FixedThreadPool和SingleThreadPool 都用到的阻塞隊列 LinkedBlockingQueue。
LinkedBlockingQueue
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
從LinkedBlockingQueue的源碼註釋中我們可以看到, 如果不指定隊列的容量, 那麼默認就是接近無限大的。
從動畫可以看出, 花瓶的瓶頸是會無限變長的, 也就是說不管瓶口容量設計得多大, 都是沒有作用的!
所以不管線程池FixedThreadPool和SingleThreadPool 的mamximumPoolSize 等於多少, 都是不生效的!
線程池CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
我們關心的參數如下
corePoolSize:0
mamximumPoolSize:Integer.MAX_VALUE
workQueue:SynchronousQueue
表示的花瓶就是下圖這樣子:
這裏我們由發現了一個問題:
爲什麼CachedThreadPool的mamximumPoolSize要設計成接近無限大的?
回答這個問題, 我們再看一下線程池CachedThreadPool的 workQueue 參數:SynchronousQueue。
SynchronousQueue
來看SynchronousQueue的源碼註釋:
A synchronous queue does not have any internal capacity, not even a capacity of one.
從註釋中我們可以看到, 同步隊列可以認爲是容量爲0。一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態。
所以如果mamximumPoolSize不設計得很大, 就很容易導致溢出。
但是瓶口設置得太大,堆積的小球太多,又會導致OOM(內存溢出)。
線程池ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
0, NANOSECONDS,
new DelayedWorkQueue());
}
我們關心的參數如下
corePoolSize:corePoolSize
mamximumPoolSize:Integer.MAX_VALUE
workQueue:DelayedWorkQueue
可以看到, 這裏出現了一個新的隊列 workQueue:DelayedWorkQueue
DelayedWorkQueue 是無界隊列, 基於數組實現, 隊列的長度可以擴容到 Integer.MAX_VALUE。
同時ScheduledThreadPool的 mamximumPoolSize 也是接近無限大的。
可以想象得到,ScheduledThreadPool就是史上最強花瓶, 極端情況下長度已經突破天際了!
到這裏, 相信大家已經明白, 爲什麼這四種線程會導致OOM了。
線程池的狀態
狀態:
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
線程池狀態含義:
-
RUNNING:接受新任務並且處理阻塞隊列裏的任務;
-
SHUTDOWN:拒絕新任務但是處理阻塞隊列裏的任務;
-
STOP:拒絕新任務並且拋棄阻塞隊列裏的任務,同時會中斷正在處理的任務;
-
TIDYING:所有任務都執行完(包含阻塞隊列裏面任務)當前線程池活動線程爲 0,將要調用
terminated
方法; -
TERMINATED:終止狀態,terminated方法調用完成以後的狀態。
線程池狀態轉換:
1.RUNNING -> SHUTDOWN:顯式調用 shutdown()
方法,或者隱式調用了 finalize()
,它裏面調用了 shutdown()
方法。
2.RUNNING or SHUTDOWN -> STOP:顯式調用 shutdownNow()
方法時候。
3.SHUTDOWN -> TIDYING:當線程池和任務隊列都爲空的時候。
4.STOP -> TIDYING:當線程池爲空的時候。
5.TIDYING -> TERMINATED:當 terminated() hook
方法執行完成時候。
線程池處理流程和原理
我們先看提交任務的源碼:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
通過上面可視化的展現幾個參數,我們大概瞭解它們的意思
線程池的原理,當提交一個新的任務到線程池時,線程池的處理流程如下:
執行ThreadPoolExcutor的execute方法,可能會遇到以下情況:
- 如果線程池中的線程數未達到核心線程數,則創建核心線程處理任務。
- 如果線程數大於或者等於核心線程數,則將任務加入任務隊列中,線程池中的空閒線程會不斷的從任務隊列中取出任務進行處理。
- 如果任務隊列滿了,並且線程數沒有達到最大線程數,則創建非核心線程去處理任務。
- 如果線程數超過了最大線程數,則執行上面提到的幾種飽和策略。
如何配置線程池:
- CPU密集型任務:儘量使用較小的線程池,一般爲CPU核心數+1。 因爲CPU密集型任務使得CPU使用率很高,若開過多的線程數,會造成CPU過度切換。
- IO密集型任務:可以使用稍大的線程池,一般爲2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候有其他線程去處理別的任務,充分利用CPU時間。
- 混合型任務:可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的線程池去處理。 只要分完之後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。因爲如果劃分之後兩個任務執行時間有數據級的差距,那麼拆分沒有意義。因爲先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,而且還要加上任務拆分與合併的開銷,得不償失。