1.執行緒池的作用
一方面當執行大量非同步任務時候執行緒池能夠提供較好的效能,在不使用執行緒池的時候,每當需要執行非同步任務時候是直接 new 一執行緒進行執行,而執行緒的建立和銷燬是需要開銷的。使用執行緒池時候,執行緒池裡面的執行緒是可複用的,不會每次執行非同步任務時候都重新建立和銷燬執行緒。
另一方面執行緒池提供了一種資源限制和管理的手段,比如可以限制執行緒的個數,動態新增執行緒等,每個 ThreadPoolExecutor
也保留了一些基本的統計資料,比如當前執行緒池完成的任務數目等。
2.ThreadPoolExecutor 原理探究
類圖如下:
如上類圖,Executors 其實是個工具類,裡面提供了好多靜態方法,根據使用者選擇返回不同的執行緒池例項。
ThreadPoolExecutor
繼承了 AbstractExecutorService
,成員變數 ctl 是個 Integer 的原子變數用來記錄執行緒池狀態 和 執行緒池中執行緒個數,類似於 ReentrantReadWriteLock
使用一個變數存放兩種資訊。
這裡假設 Integer 型別是 32 位二進位制標示,則其中高 3 位用來表示執行緒池狀態,後面 29 位用來記錄執行緒池執行緒個數。
//用來標記執行緒池狀態(高3位),執行緒個數(低29位) //預設是RUNNING狀態,執行緒個數為0 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //執行緒個數掩碼位數,並不是所有平臺int型別是32位,所以準確說是具體平臺下Integer的二進位制位數-3後的剩餘位數才是執行緒的個數, private static final int COUNT_BITS = Integer.SIZE - 3; //執行緒最大個數(低29位)00011111111111111111111111111111 private static final int CAPACITY = (1 << COUNT_BITS) - 1;
執行緒池狀態:
//(高3位):11100000000000000000000000000000 private static final int RUNNING = -1 << COUNT_BITS; //(高3位):00000000000000000000000000000000 private static final int SHUTDOWN = 0 << COUNT_BITS; //(高3位):00100000000000000000000000000000 private static final int STOP = 1 << COUNT_BITS; //(高3位):01000000000000000000000000000000 private static final int TIDYING = 2 << COUNT_BITS; //(高3位):01100000000000000000000000000000 private static final int TERMINATED = 3 << COUNT_BITS; // 獲取高三位 執行狀態 private static int runStateOf(int c) { return c & ~CAPACITY; } //獲取低29位 執行緒個數 private static int workerCountOf(int c) { return c & CAPACITY; } //計算ctl新值,執行緒狀態 與 執行緒個數 private static int ctlOf(int rs, int wc) { return rs | wc; }
執行緒池狀態含義:
-
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
方法執行完成時候。
執行緒池引數:
-
corePoolSize:執行緒池核心執行緒個數;
-
workQueue:用於儲存等待執行的任務的阻塞佇列;比如基於陣列的有界
ArrayBlockingQueue
,基於連結串列的無界LinkedBlockingQueue
,最多隻有一個元素的同步佇列SynchronousQueue
,優先順序佇列PriorityBlockingQueue
等。
-
maximunPoolSize:執行緒池最大執行緒數量。
-
ThreadFactory:建立執行緒的工廠。
-
RejectedExecutionHandler:飽和策略,當佇列滿了並且執行緒個數達到
maximunPoolSize
後採取的策略,比如AbortPolicy
(丟擲異常),CallerRunsPolicy
(使用呼叫者所線上程來執行任務),DiscardOldestPolicy
(呼叫 poll 丟棄一個任務,執行當前任務),DiscardPolicy
(默默丟棄,不丟擲異常)。 -
keeyAliveTime:存活時間。如果當前執行緒池中的執行緒數量比核心執行緒數量要多,並且是閒置狀態的話,這些閒置的執行緒能存活的最大時間。
-
TimeUnit,存活時間的時間單位。
執行緒池型別:
1.newFixedThreadPool:建立一個核心執行緒個數和最大執行緒個數都為 nThreads 的執行緒池,並且阻塞佇列長度為 Integer.MAX_VALUE
,keeyAliveTime=0
說明只要執行緒個數比核心執行緒個數多並且當前空閒則回收。程式碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } //使用自定義執行緒建立工廠 public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
2.newSingleThreadExecutor:建立一個核心執行緒個數和最大執行緒個數都為1的執行緒池,並且阻塞佇列長度為 Integer.MAX_VALUE
,keeyAliveTime=0
說明只要執行緒個數比核心執行緒個數多並且當前空閒則回收。程式碼如下:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } //使用自己的執行緒工廠 public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
3.newCachedThreadPool:建立一個按需建立執行緒的執行緒池,初始執行緒個數為 0,最多執行緒個數為 Integer.MAX_VALUE
,並且阻塞佇列為同步佇列,keeyAliveTime=60
說明只要當前執行緒 60s 內空閒則回收。這個特殊在於加入到同步佇列的任務會被馬上被執行,同步佇列裡面最多隻有一個任務。程式碼如下:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } //使用自定義的執行緒工廠 public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
如類圖,其中 mainLock 是獨佔鎖,用來控制新增 Worker 執行緒時候的原子性,termination 是該鎖對應的條件佇列,線上程呼叫 awaitTermination
時候用來存放阻塞的執行緒。
Worker 繼承 AQS 和 Runnable 介面,是具體承載任務的物件,Worker 繼承了 AQS,自己實現了簡單不可重入獨佔鎖,其中 status=0
標示鎖未被獲取狀態,state=1
標示鎖已經被獲取的狀態,state=-1
是建立 Worker 時候預設的狀態,建立時候狀態設定為 -1 是為了避免在該執行緒在執行 runWorker()
方法前被中斷,下面會具體講解到。其中變數 firstTask 記錄該工作執行緒執行的第一個任務,thread 是具體執行任務的執行緒。
DefaultThreadFactory
是執行緒工廠,newThread
方法是對執行緒的一個修飾,其中 poolNumber
是個靜態的原子變數,用來統計執行緒工廠的個數,threadNumber
用來記錄每個執行緒工廠建立了多少執行緒,這兩個值也作為執行緒池和執行緒的名稱的一部分。
3.原始碼分析
1 public void execute(Runnable command):execute
方法是提交任務 command 到執行緒池進行執行,使用者執行緒提交任務到執行緒池的模型圖如下所示:
如上圖可知 ThreadPoolExecutor
的實現實際是一個生產消費模型,其中當使用者新增任務到執行緒池時候相當於生產者生產元素,workers 執行緒工作集中的執行緒直接執行任務或者從任務佇列裡面獲取任務相當於消費者消費元素。使用者執行緒提交任務的 execute
方法具體程式碼如下:
public void execute(Runnable command) { //(1) 如果任務為null,則丟擲NPE異常 if (command == null) throw new NullPointerException(); //(2)獲取當前執行緒池的狀態+執行緒個數變數的組合值 int c = ctl.get(); //(3)當前執行緒池執行緒個數是否小於corePoolSize,小於則開啟新執行緒執行 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //(4)如果執行緒池處於RUNNING狀態,則新增任務到阻塞佇列 if (isRunning(c) && workQueue.offer(command)) { //(4.1)二次檢查 int recheck = ctl.get(); //(4.2)如果當前執行緒池狀態不是RUNNING則從佇列刪除任務,並執行拒絕策略 if (! isRunning(recheck) && remove(command)) reject(command); //(4.3)否者如果當前執行緒池執行緒空,則新增一個執行緒 else if (workerCountOf(recheck) == 0) addWorker(null, false); } //(5)如果佇列滿了,則新增執行緒,新增失敗則執行拒絕策略 else if (!addWorker(command, false)) reject(command); }
1.程式碼(3)判斷如果當前執行緒池執行緒個數小於 corePoolSize,如上圖會在 workers 裡面新增一個核心執行緒(core 執行緒)執行該任務。
2.如果當前執行緒池執行緒個數大於等於 corePoolSize 執行程式碼(4),如果當前執行緒池處於 RUNNING 狀態則新增當前任務到任務佇列,這裡需要判斷執行緒池狀態是因為有可能執行緒池已經處於非 RUNNING 狀態,而非 RUNNING 狀態下是拋棄新任務的。
3.如果任務新增任務佇列成功,則程式碼(4.2)對執行緒池狀態進行二次校驗,這是因為新增任務到任務佇列後,執行程式碼(4.2)前有可能執行緒池的狀態已經變化了,這裡進行二次校驗,如果當前執行緒池狀態不是 RUNNING 了則把任務從任務佇列移除,移除後執行拒絕策略;如果二次校驗通過,則執行程式碼(4.3)重新判斷當前執行緒池裡面是否還有執行緒,如果沒有則新增一個執行緒。
4.如果程式碼(4)新增任務失敗,則說明任務佇列滿了,則執行程式碼(5)嘗試新開啟執行緒(如上圖 thread 3 和 thread 4)來執行該任務,如果當前執行緒池執行緒個數 > maximumPoolSize
則執行拒絕策略。
接下來看新增執行緒的 addWorkder
方法的原始碼,如下:
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (;;) { int c = ctl.get(); int rs = runStateOf(c); //(6) 檢查佇列是否只在必要時為空 if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false; //(7)迴圈cas增加執行緒個數 for (;;) { int wc = workerCountOf(c); //(7.1)如果執行緒個數超限則返回false if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false; //(7.2)cas增加執行緒個數,同時只有一個執行緒成功 if (compareAndIncrementWorkerCount(c)) break retry; //(7.3)cas失敗了,則看執行緒池狀態是否變化了,變化則跳到外層迴圈重試重新獲取執行緒池狀態,否者內層迴圈重新cas。 c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; } } //(8)到這裡說明cas成功了 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //(8.1)建立worker final ReentrantLock mainLock = this.mainLock; w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { //(8.2)加獨佔鎖,為了workers同步,因為可能多個執行緒呼叫了執行緒池的execute方法。 mainLock.lock(); try { //(8.3)重新檢查執行緒池狀態,為了避免在獲取鎖前呼叫了shutdown介面 int c = ctl.get(); int rs = runStateOf(c); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); //(8.4)新增任務 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } //(8.5)新增成功則啟動任務 if (workerAdded) { t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
如上程式碼主要分兩部分,第一部分的雙重迴圈目的是通過 cas 操作增加執行緒池執行緒數,第二部分主要是併發安全的把任務新增到 workers 裡面,並且啟動任務執行。
先看第一部分的程式碼(6),如下所示:
rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())
這樣看不好理解,我們展開!運算子後,相當於:
s >= SHUTDOWN && (rs != SHUTDOWN ||//(1) firstTask != null ||//(2) workQueue.isEmpty())//(3)
如上程式碼,也就是說程式碼(6)在下面幾種情況下會返回 false:
1.當前執行緒池狀態為 STOP,TIDYING,TERMINATED;
2.當前執行緒池狀態為 SHUTDOWN 並且已經有了第一個任務;
3.當前執行緒池狀態為 SHUTDOWN 並且任務佇列為空。
回到上面看新增執行緒的 addWorkder
方法,發現內層迴圈作用是使用 cas 增加執行緒,程式碼(7.1)如果執行緒個數超限則返回 false,否者執行程式碼(7.2)執行 CAS 操作設定執行緒個數,cas 成功則退出雙迴圈,CAS 失敗則執行程式碼(7.3)看當前執行緒池的狀態是否變化了,如果變了,則重新進入外層迴圈重新獲取執行緒池狀態,否者進入內層迴圈繼續進行 cas 嘗試。
執行到第二部分的程式碼(8)說明使用 CAS 成功的增加了執行緒個數,但是現在任務還沒開始執行,這裡使用全域性的獨佔鎖來控制把新增的 Worker 新增到工作集 workers。程式碼(8.1)建立了一個工作執行緒 Worker。
程式碼(8.2)獲取了獨佔鎖,程式碼(8.3)重新檢查執行緒池狀態,這是為了避免在獲取鎖前其他執行緒呼叫了 shutdown 關閉了執行緒池,如果執行緒池已經被關閉,則釋放鎖,新增執行緒失敗,否者執行程式碼(8.4)新增工作執行緒到執行緒工作集,然後釋放鎖,程式碼(8.5)如果判斷如果工作執行緒新增成功,則啟動工作執行緒。
3.2 工作執行緒 Worker 的執行
當使用者執行緒提交任務到執行緒池後,具體是使用 worker 來執行的,先看下 Worker 的建構函式:
Worker(Runnable firstTask) { setState(-1); // 在呼叫runWorker前禁止中斷 this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this);//建立一個執行緒 }
如上程式碼建構函式內首先設定 Worker 的狀態為 -1,是為了避免當前 worker 在呼叫 runWorker
方法前被中斷(當其它執行緒呼叫了執行緒池的 shutdownNow 時候,如果 worker 狀態 >= 0 則會中斷該執行緒)。這裡設定了執行緒的狀態為 -1,所以該執行緒就不會被中斷了。如下程式碼執行 runWorker 的程式碼(9)時候會呼叫 unlock 方法,該方法把 status 變為了 0,所以這時候呼叫 shutdownNow 會中斷 worker 執行緒了。
接著我們再看看runWorker方法,程式碼如下:
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); //(9)status設定為0,允許中斷 boolean completedAbruptly = true; try { //(10) while (task != null || (task = getTask()) != null) { //(10.1) w.lock(); ... try { //(10.2)任務執行前幹一些事情 beforeExecute(wt, task); Throwable thrown = null; try { task.run();//(10.3)執行任務 } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { //(10.4)任務執行完畢後幹一些事情 afterExecute(task, thrown); } } finally { task = null; //(10.5)統計當前worker完成了多少個任務 w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { //(11)執行清工作 processWorkerExit(w, completedAbruptly); } }
如上程式碼(10)如果當前 task==null 或者呼叫 getTask 從任務佇列獲取的任務返回 null,則跳轉到程式碼(11)執行。如果 task 不為 null 則執行程式碼(10.1)獲取工作執行緒內部持有的獨佔鎖,然後執行擴充套件介面程式碼(10.2)在具體任務執行前做一些事情,程式碼(10.3)具體執行任務,程式碼(10.4)在任務執行完畢後做一些事情,程式碼(10.5)統計當前 worker 完成了多少個任務,並釋放鎖。
這裡在執行具體任務期間加鎖,是為了避免任務執行期間,其他執行緒呼叫了 shutdown 或者 shutdownNow 命令關閉了執行緒池。
其中程式碼(11)執行清理任務,其程式碼如下:
private void processWorkerExit(Worker w, boolean completedAbruptly) { ...程式碼太長,這裡就不展示了 //(11.1)統計整個執行緒池完成的任務個數,並從工作集裡面刪除當前woker final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; workers.remove(w); } finally { mainLock.unlock(); } //(11.2)嘗試設定執行緒池狀態為TERMINATED,如果當前是shutdonw狀態並且工作佇列為空 //或者當前是stop狀態當前執行緒池裡面沒有活動執行緒 tryTerminate(); //(11.3)如果當前執行緒個數小於核心個數,則增加 int c = ctl.get(); if (runStateLessThan(c, STOP)) { if (!completedAbruptly) { int min = allowCoreThreadTimeOut ? 0 : corePoolSize; if (min == 0 && ! workQueue.isEmpty()) min = 1; if (workerCountOf(c) >= min) return; // replacement not needed } addWorker(null, false); } }
如上程式碼(11.1)統計執行緒池完成任務個數,可知在統計前加了全域性鎖,把當前工作執行緒中完成的任務累加到全域性計數器,然後從工作集中刪除當前 worker。
程式碼(11.2)判斷如果當前執行緒池狀態是 shutdonw 狀態並且工作佇列為空或者當前是 stop 狀態當前執行緒池裡面沒有活動執行緒則設定執行緒池狀態為 TERMINATED,如果設定為了 TERMINATED 狀態還需要呼叫條件變數 termination 的 signalAll()
方法啟用所有因為呼叫執行緒池的 awaitTermination
方法而被阻塞的執行緒
程式碼(11.3)則判斷當前執行緒裡面執行緒個數是否小於核心執行緒個數,如果是則新增一個執行緒。
3.3 shutdown 操作:呼叫 shutdown 後,執行緒池就不會在接受新的任務了,但是工作佇列裡面的任務還是要執行的,該方法立刻返回的,並不等待佇列任務完成在返回。程式碼如下:
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //(12)許可權檢查 checkShutdownAccess(); //(13)設定當前執行緒池狀態為SHUTDOWN,如果已經是SHUTDOWN則直接返回 advanceRunState(SHUTDOWN); //(14)設定中斷標誌 interruptIdleWorkers(); onShutdown(); } finally { mainLock.unlock(); } //(15)嘗試狀態變為TERMINATED tryTerminate(); }
如上程式碼(12)檢查如果設定了安全管理器,則看當前呼叫 shutdown 命令的執行緒是否有關閉執行緒的許可權,如果有許可權則還要看呼叫執行緒是否有中斷工作執行緒的許可權,如果沒有許可權則丟擲 SecurityException
或者 NullPointerException
異常。
其中程式碼(13)內容如下,如果當前狀態 >= SHUTDOWN 則直接返回,否者設定當前狀態為 SHUTDOWN:
private void advanceRunState(int targetState) { for (;;) { int c = ctl.get(); if (runStateAtLeast(c, targetState) || ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) break; } }
程式碼(14)內容如下,設定所有空閒執行緒的中斷標誌,這裡首先加了全域性鎖,同時只有一個執行緒可以呼叫 shutdown 設定中斷標誌,然後嘗試獲取 worker 自己的鎖,獲取成功則設定中斷標識,由於正在執行的任務已經獲取了鎖,所以正在執行的任務沒有被中斷。這裡中斷的是阻塞到 getTask()
方法,企圖從佇列裡面獲取任務的執行緒,也就是空閒執行緒。
private void interruptIdleWorkers(boolean onlyOne) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) { Thread t = w.thread; //如果工作執行緒沒有被中斷,並且沒有正在執行則設定設定中斷 if (!t.isInterrupted() && w.tryLock()) { try { t.interrupt(); } catch (SecurityException ignore) { } finally { w.unlock(); } } if (onlyOne) break; } } finally { mainLock.unlock(); } }
程式碼(15)判斷如果當前執行緒池狀態是 shutdonw 狀態並且工作佇列為空或者當前是 stop 狀態當前執行緒池裡面沒有活動執行緒則設定執行緒池狀態為 TERMINATED,如果設定為了 TERMINATED 狀態還需要呼叫條件變數 termination 的 signalAll()
方法啟用所有因為呼叫執行緒池的 awaitTermination 方法而被阻塞的執行緒
3.4 shutdownNow 操作
呼叫 shutdownNow 後,執行緒池就不會在接受新的任務了,並且丟棄工作佇列裡面裡面的任務,正在執行的任務會被中斷,該方法是立刻返回的,並不等待啟用的任務執行完成在返回。返回值為這時候佇列裡面被丟棄的任務列表。程式碼如下:
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess();//(16)許可權檢查 advanceRunState(STOP);//(17) 設定執行緒池狀態為stop interruptWorkers();//(18)中斷所有執行緒 tasks = drainQueue();//(19)移動佇列任務到tasks } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
如上程式碼首先呼叫程式碼(16)檢查許可權,然後呼叫程式碼(17)設定當前執行緒池狀態為 stop,然後執行程式碼(18)中斷所有的工作執行緒,這裡需要注意的是中斷所有的執行緒,包含空閒執行緒和正在執行任務的執行緒,程式碼如下:
private void interruptWorkers() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (Worker w : workers) w.interruptIfStarted(); } finally { mainLock.unlock(); } }
然後程式碼(19)移動當前任務佇列裡面任務到 tasks 列表。
3.4 awaitTermination 操作
當執行緒呼叫 awaitTermination 方法後,當前執行緒會被阻塞,知道執行緒池狀態變為了 TERMINATED 才返回,或者等待時間超時才返回,整個過程獨佔鎖,程式碼如下:
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { for (;;) { if (runStateAtLeast(ctl.get(), TERMINATED)) return true; if (nanos <= 0) return false; nanos = termination.awaitNanos(nanos); } } finally { mainLock.unlock(); } }
如上程式碼首先獲取了獨佔鎖,然後無限迴圈內部首先判斷當前執行緒池狀態是否至少是 TERMINATED 狀態,如果是則直接返回。否者說明當前執行緒池裡面還有執行緒在執行,則看設定的超時時間 nanos 是否小於 0,小於 0 則說明不需要等待,則直接返回;如果大於0則呼叫條件變數 termination 的 awaitNanos 方法等待 nanos 時間,期望在這段時間內執行緒池狀態內變為 TERMINATED 狀態。
在講解 shutdown 方法時候提到當執行緒池狀態變為 TERMINATED 後,會呼叫 termination.signalAll()
用來啟用呼叫條件變數 termination 的 await 系列方法被阻塞的所有執行緒,所以如果在呼叫了 awaitTermination
之後呼叫了 shutdown
方法,並且 shutdown 內部設定執行緒池狀態為 TERMINATED
了,則 termination.awaitNanos
方法會返回。
另外在工作執行緒 Worker 的 runWorker 方法內當工作執行緒執行結束後,會呼叫 processWorkerExit 方法,processWorkerExit 方法內部也會呼叫 tryTerminate 方法測試當前是否應該把執行緒池設定為 TERMINATED 狀態,如果是,則也會呼叫 termination.signalAll()
用來啟用呼叫執行緒池的 awaitTermination
方法而被阻塞的執行緒
另外當等待時間超時後,termination.awaitNanos
也會返回,這時候會重新檢查當前執行緒池狀態是否為 TERMINATED,如果是則直接返回,否者繼續阻塞掛起自己。
4、使用執行緒池需要注意的地方
4.1 建立執行緒池時候要指定與業務相關的名字,以便於追溯問題
日常開發中當一個應用中需要建立多個執行緒池時候最好給執行緒池根據業務型別設定具體的名字,以便在出現問題時候方便進行定位,下面就通過例項來說明不設定時候為何難以定位問題,以及如何進行設定。
下面通過簡單的程式碼來說明不指定執行緒池名稱為何難定位問題,程式碼如下:
package com.hjc; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Created by cong on 2019/5/26. */ public class ThreadPoolExecutorTest { static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); public static void main(String[] args) { //接受使用者連結模組 executorOne.execute(new Runnable() { public void run() { System.out.println("接受使用者連結執行緒"); throw new NullPointerException(); } }); //具體處理使用者請求模組 executorTwo.execute(new Runnable() { public void run() { System.out.println("具體處理業務請求執行緒"); } }); executorOne.shutdown(); executorTwo.shutdown(); } }
執行程式碼輸出如下結果:
同理我們並不知道是那個模組的執行緒池丟擲了這個異常,那麼我們看下這個 pool-1-thread-1
是如何來的。其實是使用了執行緒池預設的 ThreadFactory,翻看執行緒池建立的原始碼如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public static ThreadFactory defaultThreadFactory() { return new DefaultThreadFactory(); } static class DefaultThreadFactory implements ThreadFactory { //(1) private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; //(2) private final AtomicInteger threadNumber = new AtomicInteger(1); //(3) private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { //(4) Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
如上程式碼 DefaultThreadFactory
的實現可知:
1.程式碼(1)poolNumber 是 static 的原子變數用來記錄當前執行緒池的編號,它是應用級別的,所有執行緒池公用一個,比如建立第一個執行緒池時候執行緒池編號為1,建立第二個執行緒池時候執行緒池的編號為2,這裡 pool-1-thread-1
裡面的 pool-1 中的 1 就是這個值。
2.程式碼(2)threadNumber 是執行緒池級別的,每個執行緒池有一個該變數用來記錄該執行緒池中執行緒的編號,這裡 pool-1-thread-1
裡面的 thread - 1 中的 1 就是這個值。
3.程式碼(3)namePrefix是執行緒池中執行緒的字首,預設固定為pool。
4.程式碼(4)具體建立執行緒,可知執行緒的名稱使用 namePrefix + threadNumber.getAndIncrement()
拼接的。
從上知道我們只需對 DefaultThreadFactory
的程式碼中 namePrefix 的初始化做手腳,當需要建立執行緒池是傳入與業務相關的 namePrefix 名稱就可以了,程式碼如下:
package com.hjc; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /** * Created by cong on 2019/5/26. */ // 命名執行緒工廠 public class HjcThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; HjcThreadFactory(String name) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); if (null == name || name.isEmpty()) { name = "pool"; } namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
然後建立執行緒池時候如下:
package com.hjc; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Created by cong on 2019/5/26. */ public class ThreadPoolExecutorTest { static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(),new HjcThreadFactory("ASYN-ACCEPT-POOL")); static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(),new HjcThreadFactory("ASYN-PROCESS-POOL")); public static void main(String[] args) { //接受使用者連結模組 executorOne.execute(new Runnable() { public void run() { System.out.println("接受使用者連結執行緒"); throw new NullPointerException(); } }); //具體處理使用者請求模組 executorTwo.execute(new Runnable() { public void run() { System.out.println("具體處理業務請求執行緒"); } }); executorOne.shutdown(); executorTwo.shutdown(); } }
然後執行執行結果如下:
從執行結果丟擲的異常,可以看到從 ASYN-ACCEPT-POOL-1-thread-1
就可以知道是接受連結執行緒池丟擲的異常。
4.4 執行緒池中使用 ThreadLocal 導致的記憶體洩露
下面先看執行緒池中使用 ThreadLocal 的例子:
package com.hjc; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; /** * Created by cong on 2019/5/26. */ public class ThreadPoolTest { static class LocalVariable { private Long[] a = new Long[1024 * 1024]; } // (1) final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); // (2) final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); public static void main(String[] args) throws InterruptedException { // (3) for (int i = 0; i < 50; ++i) { poolExecutor.execute(new Runnable() { public void run() { // (4) localVariable.set(new LocalVariable()); // (5) System.out.println("use local varaible"); //localVariable.remove(); } }); Thread.sleep(1000); } // (6) System.out.println("pool execute over"); } }
程式碼(1)建立了一個核心執行緒數和最大執行緒數為 5 的執行緒池,這個保證了執行緒池裡面隨時都有 5 個執行緒在執行。
程式碼(2)建立了一個 ThreadLocal 的變數,泛型引數為 LocalVariable,LocalVariable 內部是一個 Long 陣列。
程式碼(3)向執行緒池裡面放入 50 個任務
程式碼(4)設定當前執行緒的 localVariable 變數,也就是把 new 的 LocalVariable 變數放入當前執行緒的 threadLocals 變數。
由於沒有呼叫執行緒池的 shutdown 或者 shutdownNow 方法所以執行緒池裡面的使用者執行緒不會退出,進而 JVM 程式也不會退出。
執行當前程式碼,使用 jconsole 監控堆記憶體變化如下圖:
然後解開 localVariable.remove()
註釋,然後在執行,觀察堆記憶體變化如下:
從執行結果一可知,當主執行緒處於休眠時候程式佔用了大概 77M 記憶體,執行結果二則佔用了大概 25M 記憶體,可知執行程式碼一時候記憶體發生了洩露,下面分析下洩露的原因。
執行結果一的程式碼,在設定執行緒的 localVariable 變數後沒有呼叫 localVariable.remove()
方法,導致執行緒池裡面的 5 個執行緒的 threadLocals 變數裡面的 new LocalVariable()
例項沒有被釋放,雖然執行緒池裡面的任務執行完畢了,但是執行緒池裡面的 5 個執行緒會一直存在直到 JVM 程式被殺死。
這裡需要注意的是由於 localVariable 被宣告瞭 static,雖然執行緒的 ThreadLocalMap 裡面是對localVariable的弱引用,localVariable也不會被回收。
執行結果二的程式碼由於執行緒在設定 localVariable 變數後及時呼叫了 localVariable.remove()
方法進行了清理,所以不會存在記憶體洩露。
總結:執行緒池裡面設定了 ThreadLocal 變數一定要記得及時清理,因為執行緒池裡面的核心執行緒是一直存在的,如果不清理,那麼執行緒池的核心執行緒的 threadLocals 變數一直會持有 ThreadLocal 變數。