線程池數據結構與線程構造方法java
因爲已經看到了ThreadPoolExecutor的源碼,所以很容易就看到了ThreadPoolExecutor線程池的數據結構。圖1描述了這種數據結構。緩存
圖1 ThreadPoolExecutor 數據結構安全
其實,即便沒有上述圖形描述ThreadPoolExecutor的數據結構,咱們根據線程池的要求也很可以猜想出其數據結構出來。數據結構
- 線程池須要支持多個線程併發執行,所以有一個線程集合Collection<Thread>來執行線程任務;
- 涉及任務的異步執行,所以須要有一個集合來緩存任務隊列Collection<Runnable>;
- 很顯然在多個線程之間協調多個任務,那麼就須要一個線程安全的任務集合,同時還須要支持阻塞、超時操做,那麼BlockingQueue是必不可少的;
- 既然是線程池,出發點就是提升系統性能同時下降資源消耗,那麼線程池的大小就有限制,所以須要有一個核心線程池大小(線程個數)和一個最大線程池大小(線程個數),有一個計數用來描述當前線程池大小;
- 若是是有限的線程池大小,那麼長時間不使用的線程資源就應該銷燬掉,這樣就須要一個線程空閒時間的計數來描述線程什麼時候被銷燬;
- 前面描述過線程池也是有生命週期的,所以須要有一個狀態來描述線程池當前的運行狀態;
- 線程池的任務隊列若是有邊界,那麼就須要有一個任務拒絕策略來處理過多的任務,同時在線程池的銷燬階段也須要有一個任務拒絕策略來處理新加入的任務;
- 上面種的線程池大小、線程空閒實際那、線程池運行狀態等等狀態改變都不是線程安全的,所以須要有一個全局的鎖(mainLock)來協調這些競爭資源;
- 除了以上數據結構之外,ThreadPoolExecutor還有一些狀態用來描述線程池的運行計數,例如線程池運行的任務數、曾經達到的最大線程數,主要用於調試和性能分析。
對於ThreadPoolExecutor而言,一個線程就是一個Worker對象,它與一個線程綁定,當Worker執行完畢就是線程執行完畢,這個在後面詳細討論線程池中線程的運行方式。併發
既然是線程池,那麼就首先研究下線程的構造方法。異步
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadPoolExecutor使用一個線程工廠來構造線程。線程池都是提交一個任務Runnable,而後在某一個線程Thread中執行,ThreadFactory 負責如何建立一個新線程。ide
在J.U.C中有一個通用的線程工廠java.util.concurrent.Executors.DefaultThreadFactory,它的構造方式以下:性能
static class DefaultThreadFactory implements ThreadFactory {
static final AtomicInteger poolNumber = new AtomicInteger(1);
final ThreadGroup group;
final AtomicInteger threadNumber = new AtomicInteger(1);
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) {
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;
}
}
在這個線程工廠中,同一個線程池的全部線程屬於同一個線程組,也就是建立線程池的那個線程組,同時線程池的名稱都是「pool-<poolNum>-thread-<threadNum>」,其中poolNum是線程池的數量序號,threadNum是此線程池中的線程數量序號。這樣若是使用jstack的話很容易就看到了系統中線程池的數量和線程池中線程的數量。另外對於線程池中的全部線程默認都轉換爲非後臺線程,這樣主線程退出時不會直接退出JVM,而是等待線程池結束。還有一點就是默認將線程池中的全部線程都調爲同一個級別,這樣在操做系統角度來看全部系統都是公平的,不會致使競爭堆積。this
線程池中線程生命週期spa
一個線程Worker被構造出來之後就開始處於運行狀態。如下是一個線程執行的簡版邏輯。
private final class Worker implements Runnable {
private final ReentrantLock runLock = new ReentrantLock();
private Runnable firstTask;
Thread thread;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
}
private void runTask(Runnable task) {
final ReentrantLock runLock = this.runLock;
runLock.lock();
try {
task.run();
} finally {
runLock.unlock();
}
}
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
}
當提交一個任務時,若是須要建立一個線程(什麼時候須要在下一節中探討)時,就調用線程工廠建立一個線程,同時將線程綁定到Worker工做隊列中。須要說明的是,Worker隊列構造的時候帶着一個任務Runnable,所以Worker建立時老是綁定着一個待執行任務。換句話說,建立線程的前提是有必要建立線程(任務數已經超出了線程或者強制建立新的線程,至於爲什麼強制建立新的線程後面章節會具體分析),不會平白無故建立一堆空閒線程等着任務。這是節省資源的一種方式。
一旦線程池啓動線程後(調用線程run())方法,那麼線程工做隊列Worker就從第1個任務開始執行(這時候發現構造Worker時傳遞一個任務的好處了),一旦第1個任務執行完畢,就從線程池的任務隊列中取出下一個任務進行執行。循環如此,直到線程池被關閉或者任務拋出了一個RuntimeException。
因而可知,線程池的基本原理其實也很簡單,無非預先啓動一些線程,線程進入死循環狀態,每次從任務隊列中獲取一個任務進行執行,直到線程池被關閉。若是某個線程由於執行某個任務發生異常而終止,那麼從新建立一個新的線程而已。如此反覆。
其實,線程池原理看起來簡單,可是複雜的是各類策略,例如什麼時候該啓動一個線程,什麼時候該終止、掛起、喚醒一個線程,任務隊列的阻塞與超時,線程池的生命週期以及任務拒絕策略等等。下一節將研究這些策略問題。