關於作者
郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。
文章目錄
- 一 執行緒原理
- 1.1 執行緒建立
- 1.2 執行緒排程
- 二 執行緒同步
- 2.1 volatile
- 2.2 synchronized
- 三 執行緒池
- 3.1 執行緒池排程
- 3.2 執行緒池配置
- 3.1 執行緒池監控
- 四 執行緒池應用
- 4.1 AsyncTask
- 4.2 Okhttp
本篇文章主要用來討論Java中多執行緒併發原理與實踐經驗,並不是一篇使用例子教程,這方面內容可以參考網上其他文章。
一 執行緒原理
1.1 執行緒建立
執行緒是比程式更加輕量級的排程單位,執行緒的引入可以把程式的資源分配和執行排程分開,各個執行緒既可以共享程式資源,又可以獨立排程。
通常大家都會這麼去解釋程式與執行緒的區別,在文章01Android程式框架:程式的啟動建立、啟動與排程流程中
我們剖析了程式的本質,我們這裡再簡單回憶一下。
關於程式本質的描述:
我們知道,程式碼是靜態的,有程式碼和資源組成的系統要想執行起來就需要一種動態的存在,程式就是程式的動態執行過程。何為程式?
程式就是處理執行狀態的程式碼以及相關資源的集合,包括程式碼段、檔案、訊號、CPU狀態、記憶體地址空間等。
程式使用task_struct結構體來描述,如下所示:
- 程式碼段:編譯後形成的一些指令
- 資料段:程式執行時需要的資料
- 只讀資料段:常量
- 已初始化資料段:全域性變數,靜態變數
- 未初始化資料段(bss):未初始化的全域性變數和靜態變數
- 堆疊段:程式執行時動態分配的一些記憶體
- PCB:程式資訊,狀態標識等
我們接著來看看Java執行緒的建立序列圖,如下所示:
可以看到,最終呼叫pthread庫的pthread_create()方法建立了新的執行緒,該執行緒也使用task_struct結構體來描述,但是它沒有自己獨立的地址空間,而是與其所在的程式共享地址空間和資源。
所以你可以發現,對於虛擬機器而言,除了是否具有獨立的地址空間外,程式與執行緒並沒有本質上的區別。
我們接著來看看執行緒是如何排程的。
1.2 執行緒排程
執行緒狀態流程圖圖
- NEW:建立狀態,執行緒建立之後,但是還未啟動。
- RUNNABLE:執行狀態,處於執行狀態的執行緒,但有可能處於等待狀態,例如等待CPU、IO等。
- WAITING:等待狀態,一般是呼叫了wait()、join()、LockSupport.spark()等方法。
- TIMED_WAITING:超時等待狀態,也就是帶時間的等待狀態。一般是呼叫了wait(time)、join(time)、LockSupport.sparkNanos()、LockSupport.sparkUnit()等方法。
- BLOCKED:阻塞狀態,等待鎖的釋放,例如呼叫了synchronized增加了鎖。
- TERMINATED:終止狀態,一般是執行緒完成任務後退出或者異常終止。
NEW、WAITING、TIMED_WAITING都比較好理解,我們重點說一說RUNNABLE執行態和BLOCKED阻塞態。
執行緒進入RUNNABLE執行態一般分為五種情況:
- 執行緒呼叫sleep(time)後查出了休眠時間
- 執行緒呼叫的阻塞IO已經返回,阻塞方法執行完畢
- 執行緒成功的獲取了資源鎖
- 執行緒正在等待某個通知,成功的獲得了其他執行緒發出的通知
- 執行緒處於掛起狀態,然後呼叫了resume()恢復方法,解除了掛起。
執行緒進入BLOCKED阻塞態一般也分為五種情況:
- 執行緒呼叫sleep()方法主動放棄佔有的資源
- 執行緒呼叫了阻塞式IO的方法,在該方法返回前,該執行緒被阻塞。
- 執行緒檢視獲得一個資源鎖,但是該資源鎖正被其他執行緒鎖持有。
- 執行緒正在等待某個通知
- 執行緒排程器呼叫suspend()方法將該執行緒掛起
我們再來看看和執行緒狀態相關的一些方法。
- sleep()方法讓當前正在執行的執行緒在指定時間內暫停執行,正在執行的執行緒可以通過Thread.currentThread()方法獲取。
- yield()方法放棄執行緒持有的CPU資源,將其讓給其他任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。
- wait()方法是當前執行程式碼的執行緒進行等待,將當前執行緒放入預執行佇列,並在wait()所在的程式碼處停止執行,知道接到通知或者被中斷為止。該方法可以使得呼叫該方法的執行緒釋放共享資源的鎖,
然後從執行狀態退出,進入等待佇列,直到再次被喚醒。該方法只能在同步程式碼塊裡呼叫,否則會丟擲IllegalMonitorStateException異常。 - wait(long millis)方法等待某一段時間內是否有執行緒對鎖進行喚醒,如果超過了這個時間則自動喚醒。
- notify()方法用來通知那些可能等待該物件的物件鎖的其他執行緒,該方法可以隨機喚醒等待佇列中等同一共享資源的一個執行緒,並使該執行緒退出等待佇列,進入可執行狀態。
- notifyAll()方法可以是所有正在等待佇列中等待同一共享資源的全部執行緒從等待狀態退出,進入可執行狀態,一般會是優先順序高的執行緒先執行,但是根據虛擬機器的實現不同,也有可能是隨機執行。
- join()方法可以讓呼叫它的執行緒正常執行完成後,再去執行該執行緒後面的程式碼,它具有讓執行緒排隊的作用。
二 執行緒同步
執行緒安全,通常所說的執行緒安全指的是相對的執行緒安全,它指的是對這個物件單獨的操作是執行緒安全的,我們在呼叫的時候無需做額外的保障措施。
什麼叫相對安全??
?舉個例子
我們知道Java裡的Vector是個執行緒安全的類,在多執行緒環境下對其插入、刪除和讀取都是安全的,但這僅限於每次只有一個執行緒對其操作,如果多個執行緒同時操作
Vector,那它就不再是執行緒安全的了。
final Vector<String> vector = new Vector<>();
while (true) {
for (int i = 0; i < 10; i++) {
vector.add("項:" + i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
Log.d(TAG, vector.get(i));
}
}
});
removeThread.start();
printThread.start();
if (Thread.activeCount() >= 20) {
return;
}
}
複製程式碼
但是程式卻crash了
正確的做法應該是vector物件加上同步鎖,如下:
final Vector<String> vector = new Vector<>();
while (true) {
for (int i = 0; i < 10; i++) {
vector.add("項:" + i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector){
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (vector){
for (int i = 0; i < vector.size(); i++) {
Log.d(TAG, vector.get(i));
}
}
}
});
removeThread.start();
printThread.start();
if (Thread.activeCount() >= 20) {
return;
}
}
複製程式碼
2.1 volatile
volatile也是互斥同步的一種實現,不過它非常的輕量級。
volatile有兩條關鍵的語義:
- 保證被volatile修飾的變數對所有執行緒都是可見的
- 禁止進行指令重排序
要理解volatile關鍵字,我們得先從Java的執行緒模型開始說起。如圖所示:
Java記憶體模型規定了所有欄位(這些欄位包括例項欄位、靜態欄位等,不包括區域性變數、方法引數等,因為這些是執行緒私有的,並不存在競爭)都存在主記憶體中,每個執行緒會
有自己的工作記憶體,工作記憶體裡儲存了執行緒所使用到的變數在主記憶體裡的副本拷貝,執行緒對變數的操作只能在工作記憶體裡進行,而不能直接讀寫主記憶體,當然不同記憶體之間也
無法直接訪問對方的工作記憶體,也就是說主記憶體時執行緒傳值的媒介。
我們來理解第一句話:
保證被volatile修飾的變數對所有執行緒都是可見的
如何保證可見性??
被volatile修飾的變數在工作記憶體修改後會被強制寫回主記憶體,其他執行緒在使用時也會強制從主記憶體重新整理,這樣就保證了一致性。
關於“保證被volatile修飾的變數對所有執行緒都是可見的”,有種常見的錯誤理解:
錯誤理解:由於volatile修飾的變數在各個執行緒裡都是一致的,所以基於volatile變數的運算在多執行緒併發的情況下是安全的。
這句話的前半部分是對的,後半部分卻錯了,因此它忘記考慮變數的操作是否具有原子性這一問題。
:point_up:舉個例子
private volatile int start = 0;
private void volatileKeyword() {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
start++;
}
}
};
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
Log.d(TAG, "start = " + start);
}
複製程式碼
這段程式碼啟動了10個執行緒,每次10次自增,按道理最終結果應該是100,但是結果並非如此。
為什麼會這樣?:thinking:
仔細看一下start++,它其實並非一個原子操作,簡單來看,它有兩步:
- 取出start的值,因為有volatile的修飾,這時候的值是正確的。
- 自增,但是自增的時候,別的執行緒可能已經把start加大了,這種情況下就有可能把較小的start寫回主記憶體中。
所以volatile只能保證可見性,在不符合以下場景下我們依然需要通過加鎖來保證原子性:
- 運算結果並不依賴變數當前的值,或者只有單一執行緒修改變數的值。(要麼結果不依賴當前值,要麼操作是原子性的,要麼只要一個執行緒修改變數的值)
- 變數不需要與其他狀態變數共同參與不變約束
比方說我們會線上程里加個boolean變數,來判斷執行緒是否停止,這種情況就非常適合使用volatile。
我們再來理解第二句話。
- 禁止進行指令重排序
什麼是指令重排序??
指令重排序是值指令亂序執行,即在條件允許的情況下,直接執行當前有能力立即執行的後續指令,避開為獲取下一條指令所需資料而造成的等待,通過亂序執行的技術,提供執行效率。
指令重排序繪製被volatile修飾的變數的賦值操作前,新增一個記憶體屏障,指令重排序時不能把後面的指令重排序的記憶體屏障之前的位置。
關於指令重排序不是本篇文章重點討論的內容,更多細節可以參考指令重排序。
2.2 synchronized
synchronized是互斥同步的一種實現。
synchronized:當某個執行緒訪問被synchronized標記的方法或程式碼塊時,這個執行緒便獲得了該物件的鎖,其他執行緒暫時無法訪問這個方法,只有等待這個方法執行完畢或者程式碼塊執行完畢,這個
執行緒才會釋放該物件的鎖,其他執行緒才能執行這個方法或程式碼塊。
前面我們已經說了volatile關鍵字,這裡我們舉個例子來綜合分析volatile與synchronized關鍵字的使用。
:point_up:舉個例子
public class Singleton {
//volatile保證了:1 instance在多執行緒併發的可見性 2 禁止instance在操作是的指令重排序
private volatile static Singleton instance;
public static Singleton getInstance() {
//第一次判空,保證不必要的同步
if (instance == null) {
//synchronized對Singleton加全域性所,保證每次只要一個執行緒建立例項
synchronized (Singleton.class) {
//第二次判空時為了在null的情況下建立例項
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複製程式碼
這是一個經典的DSL單例。
它的位元組碼如下:
可以看到被synchronized同步的程式碼塊,會在前後分別加上monitorenter和monitorexit,這兩個位元組碼都需要指定加鎖和解鎖的物件。
關於加鎖和解鎖的物件:
- synchronized程式碼塊 :同步程式碼塊,作用範圍是整個程式碼塊,作用物件是呼叫這個程式碼塊的物件。
- synchronized方法 :同步方法,作用範圍是整個方法,作用物件是呼叫這個方法的物件。
- synchronized靜態方法 :同步靜態方法,作用範圍是整個靜態方法,作用物件是呼叫這個類的所有物件。
- synchronized(this):作用範圍是該物件中所有被synchronized標記的變數、方法或程式碼塊,作用物件是物件本身。
- synchronized(ClassName.class) :作用範圍是靜態的方法或者靜態變數,作用物件是Class物件。
synchronized(this)新增的是物件鎖,synchronized(ClassName.class)新增的是類鎖,它們的區別如下:
物件鎖:Java的所有物件都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。執行緒進入synchronized方法的時候獲取該物件的鎖,當然如果已經有執行緒獲取了這個物件的鎖,那麼當前線
程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放物件鎖。這裡也體現了用synchronized來加鎖的好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。
類鎖:物件鎖是用來控制例項方法之間的同步,類鎖是用來控制靜態方法(或靜態變數互斥體)之間的同步。其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理
解鎖定例項方法和靜態方法的區別的。我們都知道,java類可能會有很多個物件,但是隻有1個Class物件,也就是說類的不同例項之間共享該類的Class物件。Class物件其實也僅僅是1個
java物件,只不過有點特殊而已。由於每個java物件都有1個互斥鎖,而類的靜態方法是需要Class物件。所以所謂的類鎖,不過是Class物件的鎖而已。獲取類的Class物件有好幾種,最簡
單的就是MyClass.class的方式。 類鎖和物件鎖不是同一個東西,一個是類的Class物件的鎖,一個是類的例項的鎖。也就是說:一個執行緒訪問靜態synchronized的時候,允許另一個執行緒訪
問物件的例項synchronized方法。反過來也是成立的,因為他們需要的鎖是不同的。
關不同步鎖還有ReentrantLock,eentrantLockR相對於synchronized具有等待可中斷、公平鎖等更多功能,這裡限於篇幅,不再展開。
三 執行緒池
我們知道執行緒的建立、切換與銷燬都會花費比較大代價,所以很自然的我們使用執行緒池來複用和管理執行緒。Java裡的執行緒池我們通常通過ThreadPoolExecutor來實現。
接下來我們就來分析ThreadPoolExecutor的相關原理,以及ThreadPoolExecutor在Android上的應用AsyncTask。
3.1 執行緒池排程
執行緒池有五種執行狀態,如下所示:
執行緒池狀態圖
- RUNNING:可以接受新任務,也可以處理等待佇列裡的任務。
- SHUTDOWN:不接受新任務,但可以處理等待佇列裡的任務。
- STOP:不接受新的任務,不再處理等待佇列裡的任務。中斷正在處理的任務。
- TIDYING:所有任務都已經處理完了,當前執行緒池沒有有效的執行緒,並且即將呼叫terminated()方法。
- TERMINATED:呼叫了terminated()方法,執行緒池終止。
另外,ThreadPoolExecutor是用一個AtomicInteger來記錄執行緒池狀態和執行緒池裡的執行緒數量的,如下所示:
- 低29位:用來存放執行緒數
- 高3位:用來存放執行緒池狀態
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;// 111
private static final int SHUTDOWN = 0 << COUNT_BITS;// 000
private static final int STOP = 1 << COUNT_BITS;// 001
private static final int TIDYING = 2 << COUNT_BITS;// 010
private static final int TERMINATED = 3 << COUNT_BITS;// 110
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }//執行緒池狀態
private static int workerCountOf(int c) { return c & CAPACITY; }//執行緒池當前執行緒數
private static int ctlOf(int rs, int wc) { return rs | wc; }
複製程式碼
在正式介紹執行緒池排程原理之前,我們先來回憶一下Java實現任務的兩個介面:
- Runnable:在run()方法裡完成任務,無返回值,且不會丟擲異常。
- Callable:在call()方法裡完成任務,有返回值,且可能丟擲異常。
另外,還有個Future介面,它可以對Runnable、Callable執行的任務進行判斷任務是否完成,中斷任務以及獲取任務結果的操作。我們通常會使用它的實現類FutureTask,FutureTask是一個Future、Runnable
以及Callable的包裝類。利用它可以很方便的完成Future介面定義的操作。FutureTask內部的執行緒阻塞是基於LockSupport來實現的。
我們接下來看看執行緒池是和執行任務的。
ThreadPoolExecutor排程流程圖
execute(Runnable command)
public class ThreadPoolExecutor extends AbstractExecutorService {
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1. 若執行緒池狀態是RUNNING,執行緒池大小小於配置的核心執行緒數,則可以線上程池中建立新執行緒執行新任務。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2. 若執行緒池狀態是RUNNING,執行緒池大小大於配置的核心執行緒數,則嘗試將任務插入阻塞佇列進行等待
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//若插入成功,則將次檢查執行緒池的狀態是否為RUNNING,如果不是則移除當前任務並進入拒絕策略。
if (! isRunning(recheck) && remove(command))
reject(command);
//如果執行緒池中的執行緒數為0,即執行緒池中的執行緒都執行完畢處於SHUTDOWN狀態,此時新增了一個null任務
//(因為SHUTDOWN狀態不再接受新任務)
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 若無法插入阻塞佇列,則嘗試建立新執行緒,建立失敗則進入拒絕策略。
else if (!addWorker(command, false))
reject(command);
}
}
複製程式碼
- 若執行緒池大小小於配置的核心執行緒數,則可以線上程池中建立新執行緒執行新任務。
- 若執行緒池狀態是RUNNING,執行緒池大小大於配置的核心執行緒數,則嘗試將任務插入阻塞佇列進行等待。若插入成功,為了健壯性考慮,則將次檢查執行緒池的狀態是否為RUNNING
,如果不是則移除當前任務並進入拒絕策略。如果執行緒池中的執行緒數為0,即執行緒池中的執行緒都執行完畢處於SHUTDOWN狀態,此時新增了一個null任務(因為SHUTDOWN狀態不再接受
新任務)。 - 若無法插入阻塞佇列,則嘗試建立新執行緒,建立失敗則進入拒絕策略。
這個其實很好理解,打個比方。我們公司的一個小組來完成任務,
- 如果任務數量小於小組人數(核心執行緒數),則指派小組裡人的完成;
- 如果任務數量大於小組人數,則去招聘新人來完成,則將任務加入排期等待(阻塞佇列)。
- 如果沒有排期,則試著去招新人來完成任務(最大執行緒數),如果招新人也完成不了,說明這不是人乾的活,則去找產品經理砍需求(拒絕策略)。
addWorker(Runnable firstTask, boolean core)
addWorker(Runnable firstTask, boolean core) 表示新增個Worker,Worker實現了Runnable介面,是對Thread的封裝,該方法新增完Worker後,則呼叫runWorker()來啟動執行緒。
public class ThreadPoolExecutor extends AbstractExecutorService {
private boolean addWorker(Runnable firstTask, boolean core) {
//重試標籤
retry:
for (;;) {
int c = ctl.get();
//獲取當前執行緒池狀態
int rs = runStateOf(c);
//以下情況表示不再接受新任務:1 執行緒池沒有處於RUNNING狀態 2 要執行的任務為空 3 阻塞佇列已滿
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
//獲取執行緒池當前的執行緒數
int wc = workerCountOf(c);
//如果超出容量,則不再接受新任務,core表示是否使用corePoolSize作為比較標準
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//增加執行緒數
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
//如果執行緒池狀態發生變化,重新開始迴圈
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//執行緒數增加成功,開始新增新執行緒,Worker是Thread的封裝類
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
//加鎖
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//將新啟動的執行緒新增到執行緒池中
workers.add(w);
//更新執行緒池中執行緒的數量,注意這個數量不能超過largestPoolSize
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//呼叫runWorker()方法,開始執行執行緒
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
}
複製程式碼
runWorker(Worker w)
runWorker()方法是整個阻塞佇列的核心迴圈,在這個迴圈中,執行緒池會不斷的從阻塞佇列workerQueue中取出的新的task並執行。
public class ThreadPoolExecutor extends AbstractExecutorService {
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//從阻塞佇列中不斷取出任務,如果取出的任務為空,則迴圈終止
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//該方法為空,可以重新次方法,在任務執行開始前做一些處理
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//該方法為空,可以重新次方法,在任務執行結束後做一些處理
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
//從阻塞佇列workerQueue中取出Task
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
//迴圈
for (;;) {
int c = ctl.get();
//獲取執行緒池狀態
int rs = runStateOf(c);
//以下情況停止迴圈:1 執行緒池狀態不是RUNNING(>= SHUTDOWN)2 執行緒池狀態>= STOP 或者阻塞佇列為空
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
//遞減workCount
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// 判斷執行緒的IDLE超時機制是否生效,有兩種情況:1 allowCoreThreadTimeOut = true,這是可以手動
//設定的 2 當前執行緒數大於核心執行緒數
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//根據timed來決定是以poll超時等待的方式還是以take()阻塞等待的方式從阻塞佇列中獲取任務
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
}
複製程式碼
所以你可以理解了,runWorker()方法是在新建立執行緒的run()方法裡的,而runWorker()又不斷的呼叫getTask()方法去獲取阻塞佇列裡的任務,這樣就實現了執行緒的複用。
3.2 執行緒池配置
我們先來看看ThreadPoolExecutor的構造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
複製程式碼
- int corePoolSize:核心執行緒池大小
- int maximumPoolSize:執行緒池最大容量大小
- long keepAliveTime:執行緒不活動時存活的時間
- TimeUnit unit:時間單位
- BlockingQueue workQueue:任務佇列
- ThreadFactory threadFactory:執行緒工程
- RejectedExecutionHandler handler:執行緒拒絕策略
那麼這些引數我們應該怎麼配置呢?要合理配置執行緒池就需要先了解我們的任務特性,一般說來:
- 任務性質:CPU密集型、IO密集型、混合型
- 任務優先順序:低、中、高
- 任務執行時間:短、中、長
- 任務依賴性:是否依賴其他資源,資料庫、網路
我們根據這些屬性來一一分析這些引數的配置。
首先就是核心執行緒數corePoolSize與最大執行緒數maximumPoolSize。這個的配置我們通常要考慮CPU同時執行執行緒的閾值。一旦超過這個閾值,CPU就需要花費很多
時間來完成執行緒的切換與排程,這樣會導致效能大幅下滑。
/**
* CPU核心數,注意該方法並不可靠,它返回的有可能不是真實的CPU核心數,因為CPU在某些情況下會對某些核
* 心進行睡眠處理,這種情況返回的知識已啟用的CPU核心數。
*/
private static final int NUMBER_OF_CPU = Runtime.getRuntime().availableProcessors();
/**
* 核心執行緒數
*/
private static final int corePoolSize = Math.max(2, Math.min(NUMBER_OF_CPU - 1, 4));
/**
* 最大執行緒數
*/
private static final int maximumPoolSize = NUMBER_OF_CPU * 2 + 1;
複製程式碼
至於keepAliveTime,該引數描述了執行緒不活動時存活的時間,如果是CPU密集型任務,則將時間設定的小一些,如果是IO密集型或者資料庫連線任務,則將時間設定的長一些。
我們再來看看BlockingQueue引數的配置。BlockingQueue用來描述阻塞佇列。它的方法以四種形式存在,以此來滿足不同需求。
丟擲異常 | 特殊值 | 阻塞 | 超時 |
---|---|---|---|
add(e) | offer(e) | put(e) | offer(e, time, unit) |
remove() | poll() | take() | poll(time, unit) |
element() | peek() | 不可用 | 不可用 |
它有以下特點:
- 不支援null元素
- 執行緒安全
它的實現類有:
- ArrayBlockingQueue :一個陣列實現的有界阻塞佇列,此佇列按照FIFO的原則對元素進行排序,支援公平訪問佇列(可重入鎖實現ReenttrantLock)。
- LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列,此佇列預設和最大長度為Integer.MAX_VALUE,按照FIFO的原則對元素進行排序。
- PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列,預設情況下采用自然順序排列,也可以指定Comparator。
- DelayQueue:一個支援延時獲取元素的無界阻塞佇列,建立元素時可以指定多久以後才能從佇列中獲取當前元素,常用於快取系統設計與定時任務排程等。
- SynchronousQueue:一個不儲存元素的阻塞佇列。存入操作必須等待獲取操作,反之亦然,它相當於一個傳球手,非常適合傳遞性場景。
- LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列,與LinkedBlockingQueue相比多了transfer和tryTranfer方法,該方法在有消費者等待接收元素時會立即將元素傳遞給消費者。
- LinkedBlockingDeque:一個由連結串列結構組成的雙端阻塞佇列,可以從佇列的兩端插入和刪除元素。因為出入口都有兩個,可以減少一半的競爭。適用於工作竊取的場景。
工作竊取:例如有兩個佇列A、B,各自幹自己的活,但是A效率比較高,很快把自己的活幹完了,於是勤快的A就會去竊取B的任務來幹,這是A、B會訪問同一個佇列,為了減少A、B的競爭,規定竊取者A
只從雙端佇列的尾部拿任務,被竊取者B只從雙端佇列的頭部拿任務。
我們最後來看看RejectedExecutionHandler引數的配置。
RejectedExecutionHandler用來描述執行緒數大於或等於執行緒池最大執行緒數時的拒絕策略,它的實現類有:
- ThreadPoolExecutor.AbortPolicy:預設策略,當執行緒池中執行緒的數量大於或者等於最大執行緒數時,丟擲RejectedExecutionException異常。
- ThreadPoolExecutor.DiscardPolicy:當執行緒池中執行緒的數量大於或者等於最大執行緒數時,默默丟棄掉不能執行的新任務,不報任何異常。
- ThreadPoolExecutor.CallerRunsPolicy:當執行緒池中執行緒的數量大於或者等於最大執行緒數時,如果執行緒池沒有被關閉,則直接在呼叫者的執行緒裡執行該任務。
- ThreadPoolExecutor.DiscardOldestPolicy:當執行緒池中執行緒的數量大於或者等於最大執行緒數時,丟棄阻塞佇列頭部的任務(即等待最近的任務),然後重新新增當前任務。
另外,Executors提供了一系列工廠方法用來建立執行緒池。這些執行緒是適用於不同的場景。
- newCachedThreadPool():無界可自動回收執行緒池,檢視執行緒池中有沒有以前建立的執行緒,如果有則複用,如果沒有則建立一個新的執行緒加入池中,池中的執行緒超過60s不活動則自動終止。適用於生命
週期比較短的非同步任務。 - newFixedThreadPool(int nThreads):固定大小執行緒池,與newCachedThreadPool()類似,但是池中持有固定數目的執行緒,不能隨時建立執行緒,如果建立新執行緒時,超過了固定
執行緒數,則放在佇列裡等待,直到池中的某個執行緒被移除時,才加入池中。適用於很穩定、很正規的併發執行緒,多用於伺服器。 - newScheduledThreadPool(int corePoolSize):週期任務執行緒池,該執行緒池的執行緒可以按照delay依次執行執行緒,也可以週期執行。
- newSingleThreadExecutor():單例執行緒池,任意時間內池中只有一個執行緒。
3.3 執行緒池監控
ThreadPoolExecutor裡提供了一些空方法,我們可以通過繼承ThreadPoolExecutor,複寫這些方法來實現對執行緒池的監控。
public class ThreadPoolExecutor extends AbstractExecutorService {
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
}
複製程式碼
常見的監控指標有:
- taskCount:執行緒池需要執行的任務數量。
- completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
- largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否曾經滿過。如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過。
- getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷燬,所以這個大小隻增不減。
- getActiveCount:獲取活動的執行緒數。
四 執行緒池應用
4.1 AsyncTask
AsyncTask基於ThreadPoolExecutor實現,內部封裝了Thread+Handler,多用來執行耗時較短的任務。
一個簡單的AsyncTask例子
public class AsyncTaskDemo extends AsyncTask<String, Integer, String> {
/**
* 在後臺任務開始執行之前呼叫,用於執行一些介面初始化操作,例如顯示一個對話方塊,UI執行緒。
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
}
/**
* 執行後臺執行緒,執行完成可以通過return語句返回,worker執行緒
*
* @param strings params
* @return result
*/
@Override
protected String doInBackground(String... strings) {
return null;
}
/**
* 更新進度,UI執行緒
*
* @param values progress
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
/**
* 後臺任務執行完成並通過return語句返回後會呼叫該方法,UI執行緒。
*
* @param result result
*/
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
}
/**
* 後臺任務唄取消後回撥
*
* @param reason reason
*/
@Override
protected void onCancelled(String reason) {
super.onCancelled(reason);
}
/**
* 後臺任務唄取消後回撥
*/
@Override
protected void onCancelled() {
super.onCancelled();
}
}
複製程式碼
AsyncTask的使用非常的簡單,接下來我們去分析AsyncTask的原始碼實現。
AsyncTask流程圖
AsyncTask原始碼的一開始就是個建立執行緒池的流程。
public abstract class AsyncTask<Params, Progress, Result> {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心執行緒數,最少2個,最多4個
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//執行緒不活動時的存活時間是30s
private static final int KEEP_ALIVE_SECONDS = 30;
//執行緒構建工廠,指定執行緒的名字
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
//一個由連結串列結構組成的無界阻塞佇列
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
//構建執行緒池
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
}
複製程式碼
另外,我們可以通過AsyncTask.executeOnExecutor(Executor exec, Params… params) 來自定義執行緒池。
我們再來看看構造方法。
public abstract class AsyncTask<Params, Progress, Result> {
//構造方法需要在UI執行緒裡呼叫
public AsyncTask() {
//建立一個Callable物件,WorkerRunnable實現了Callable介面
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
Result result = doInBackground(mParams);
Binder.flushPendingCommands();
return postResult(result);
}
};
//建立一個FutureTask物件,該物件用來接收mWorker的結果
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
//將執行的結果通過傳送給Handler處理,注意FutureTask的get()方法會阻塞直至結果返回
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
//內部的Handler
private static class InternalHandler extends Handler {
public InternalHandler() {
//UI執行緒的Looper
super(Looper.getMainLooper());
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
//返回結果
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
//返回進度
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
}
複製程式碼
可以看到當我們呼叫AsyncTask的構造方法時,就建立了一個FutureTask物件,它內部包裝了Callable物件(就是我們要執行的任務),並在FutureTask物件的done()方法裡
將結果傳送給Handler。
接著看看執行方法execute()。
public abstract class AsyncTask<Params, Progress, Result> {
//需要在UI執行緒裡呼叫
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
@MainThread
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
mStatus = Status.RUNNING;
//任務執行前的處理,我們可以複寫次方法
onPreExecute();
mWorker.mParams = params;
//執行任務,exec為sDefaultExecutor
exec.execute(mFuture);
return this;
}
}
複製程式碼
接著看看這個sDefaultExecutor。
可以看到sDefaultExecutor是個SerialExecutor物件,SerialExecutor實現了Executor介面。
public abstract class AsyncTask<Params, Progress, Result> {
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private static class SerialExecutor implements Executor {
//任務佇列
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
//當前執行的任務
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
//開始執行任務
scheduleNext();
}
}
protected synchronized void scheduleNext() {
//取出佇列頭的任務開始執行
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
}
複製程式碼
所以我們沒呼叫一次AsyncTask.execute()方法就將FutureTask物件新增到佇列尾部,然後會從佇列頭部取出任務放入執行緒池中執行,所以你可以看著這是一個序列執行器。
4.2 Okhttp
在Okhttp的任務排程器Dispatcher裡有關於執行緒池的配置
public final class Dispatcher {
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
}
複製程式碼
你可以看到它的配置:
- 核心執行緒數為0,最大執行緒數為Integer.MAX_VALUE,不對核心執行緒數進行限制,隨時建立新的執行緒,空閒存活時間為60s,用完即走。這也比較符合網路請求的特性。
- 阻塞佇列為SynchronousQueue,該佇列不儲存任務,只傳遞任務,所以把任務新增進去就會執行。
這其實是Excutors.newCachedThreadPool()快取池的實現。總結來說就是新任務過來進入SynchronousQueue,它是一個單工模式的佇列,只傳遞任務,不儲存任務,然後就建立
新執行緒執行任務,執行緒不活動的存活時間為60s。
Okhttp請求流程圖
在發起網路請求時,每個請求執行完成後都會呼叫client.dispatcher().finished(this)。
final class RealCall implements Call {
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
String host() {
return originalRequest.url().host();
}
Request request() {
return originalRequest;
}
RealCall get() {
return RealCall.this;
}
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//非同步請求
client.dispatcher().finished(this);
}
}
}
}
複製程式碼
我們來看看client.dispatcher().finished(this)這個方法。
public final class Dispatcher {
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//將已經結束的請求call移除正在執行的佇列calls
if (!calls.remove(call)) throw new AssertionError("Call wasn`t in-flight!");
//非同步請求promoteCalls為true
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
private void promoteCalls() {
//當前非同步請求數大於最大請求數,不繼續執行
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
//非同步等待佇列為空,不繼續執行
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
//遍歷非同步等待佇列
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
//如果沒有超過相同host的最大請求數,則複用當前請求的執行緒
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
//執行佇列達到上限,也不再執行
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
}
複製程式碼
所以你可以看到Okhttp不是用執行緒池來控制執行緒個數,執行緒池裡的執行緒執行的都是正在執行請請求,控制執行緒的是Dispatcher,Dispatcher.promoteCalls()方法通過
最大請求數maxRequests和相同host最大請求數maxRequestsPerHost來控制非同步請求不超過兩個最大值,在值範圍內不斷的將等待佇列readyAsyncCalls中的請求新增
到執行佇列runningAsyncCalls中去。