好久沒有寫博客了,今天來寫一個線程池的內容。
我們先來看一個簡單地例子:
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
executor.execute(new Running(1));
這段代碼就是我們在使用線程池的時候最基本的一個使用方式了,而這裏出現的幾個類,ThreadPoolExecutor、Executors等,究竟分別是幹什麼用的呢?本着面向對象的原則,我們有必要去了解一下整個線程池的類族設計。
一.類族設計
爲了方便起見,在瞭解類族結構的時候,我們不直接讀源碼,採用閱讀jdk api的方式,首先在源碼中不斷尋找父類和接口後,我們發現,其最高位的祖先爲Executor接口,查看接口api,其解釋如下:
這個接口的設計原則,顧名思義,就是Runnable的執行器,接口的設計者認爲我們不應該使用new Thread。。。的方式去啓動一個線程,而是採用Executor的實現類去啓動,至於其原因,我相信對於那些對面向對象思想有一定理解的童鞋一定不會覺得難以理解,這種將線程的啓動方式隱藏在實現類內部的設計,不但可以讓線程的生命週期的處理變得統一,且對於不同處理方式的線程也可以分門別類,更重要的是,他可以讓線程的啓動和線程的執行業務完成抽象分離,交由不同的開發者去處理,這也就是爲什麼jdk(或者其他第三方庫)設計的線程池(或者其他線程執行類)能夠簡單地用於我們自己開發的業務的線程的實際執行。
再來看Executor的子接口ExecutorService:
這裏說明了兩個問題:1.Service提供了管理終止的方法;2.Executors提供了關於這個接口的工廠方法。
第二點很好理解,我們在開頭的例程中使用Executors去創建一個ThreadPoolExecutor,這就很明顯是一個工程類了。
而第一點,官方也給出例程來解釋:
class NetworkService implements Runnable {
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(int port, int poolSize)
throws IOException {
serverSocket = new ServerSocket(port);
pool = Executors.newFixedThreadPool(poolSize);
}
public void run() { // run the service
try {
for (;;) {
pool.execute(new Handler(serverSocket.accept()));
}
} catch (IOException ex) {
pool.shutdown();
}
}
}
class Handler implements Runnable {
private final Socket socket;
Handler(Socket socket) { this.socket = socket; }
public void run() {
// read and service request on socket
}
}
在這個例程中,對於execute操作發生異常的情況下,強制使用shutdown來關閉線程。
另外雖然官方文檔沒有強調,但是在ExecutorService中還設計了一系列面向Callable接口的方法。關於Callable和Runnable的區別,我們以後有機會再說。
繼續看其實現類,一個抽象類AbstractExecutorService
這個類實現了一些基本的方法,但是並不實現具體的線程啓動操作。
最後就是我們本次博客的主角,ThreadPoolExecutor了。
到目前爲止,類族的結構應該已經看得比較清晰了,我們就來具體看一下,ThreadPoolExecutor的類結構吧。
二.ThreadPoolExecutor構造方法
這個類的構造方法比較多,由於不同的構造方法之間都是互相調用來調用去的,我們直接看參數最多的那個構造函數就可以啦。
看官方參數解釋:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize
- 池中所保存的線程數,包括空閒線程。
maximumPoolSize
- 池中允許的最大線程數。
keepAliveTime
- 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
unit
- keepAliveTime 參數的時間單位。
workQueue
- 執行前用於保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務。
threadFactory
- 執行程序創建新線程時使用的工廠。
handler
- 由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。
前四個參數應該不需要解釋的吧,線程池嘛,當然要告訴他,有多少主要線程,不夠用時候最多能有幾個,排隊最多排多久。
第五個參數workQueue,這個參數是任務隊列,當線程池裏面的任務跑滿的情況下,會把新execute傳入的那個Runnable扔到workQueue裏面,然後當有線程跑完了以後,再去workQueue裏面拿出來,然後。。。。反正就是這樣嘛,這玩意就是排隊用的,而且爲了避免程序員自己去傳入這個參數,通常工廠類Executors都直接把這個參數給屏蔽了,如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
工廠類直接幫我們傳了個Queue進去,你就不用自己傳了。
什麼?你問爲什麼要用LinkedBlockingQueue?因爲這玩意線程安全而且容易插入刪除,畢竟嘛。。。。嗯
至於第六個參數threadFactory
,這個參數我感覺就是個醬油,唉簡單說一說
static class DefaultThreadFactory 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;
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;
}
}
直接給你們看官方默認的生成方法,這裏你們一定要搞清楚一個概念:
Runnable是個任務,Thread是個線程,這兩玩意雖然常常一起用,但是是有概念上的區別的。
線程池可以無限的接收任務,但是線程數目有上限,如果線程滿了,就讓任務等待,任務是業務層傳入的,但是線程是線程池自己創建的。就是這麼個道理,既然要創建自然也需要一個創建的方式咯。(這裏不得不說jdk源碼的設計真的是精細,如果換做一般人,到這一步,這種可擴展的設計的情況下,默認估計就直接new Thread(r)了)。
handler
參數是指如果任務(Runnable)排隊排太久了超出了時間限制,那就調用handler方法處理並將任務移除。handler就一個回調,沒啥太大作用。
好了構造方法搞定了,那麼Executors中自然也就是調用一下構造方法,按照我們開頭的例程,代碼已經一半搞定了,接下來我們就要開始看痛苦的後一半內容——execute方法了。
三.execute(Runnable r)方法
上代碼吧:
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)) {
// 如果線程數量已經大於核心線程數量,並且線程池正在運行(沒有shutdown),那就把任務加入隊列
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);
}
這裏的拒絕reject就是調用一下之前構造器傳入的那個handler。
execute方法的操作可以看清楚,對線程池處於不同情況下,做了個分類,如果覺得註釋的不夠清楚,我們再看一看官方的邏輯:
排隊有三種通用策略:
- 直接提交。工作隊列的默認選項是
SynchronousQueue
,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。 - 無界隊列。使用無界隊列(例如,不具有預定義容量的
LinkedBlockingQueue
)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性。 - 有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如
ArrayBlockingQueue
)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能爲超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量。
然後我們來看一看他的提交任務的代碼addWorker:
private boolean addWorker(Runnable firstTask, boolean core) {
// 通過一堆循環來給原子類加1,這個數表示線程池的線程數
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
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
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 生成worker
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);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 運行線程
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
簡單說明addWorker就是,先利用CAS原子類去給線程數+1,然後新生成一個Worker去啓動線程。傳入firstTask
然後這裏我們看到一個w.thread 然後這玩意被start()了,我們看下worker的源碼。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
// ...
/** 工作線程 */
final Thread thread;
/**任務 */
Runnable firstTask;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker. */
public void run() {
runWorker(this);
}
// ...
}
然後我們就發現,這個t.start()其實就是在線程中執行worker裏的run(注意這個和傳入的runnable是個父子關係)
然後最後:
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);
}
}
循環,第一個任務處理完了,處理在隊列中等待的。
後面就不解釋了,把任務取出來,執行下pre,執行下run,執行下after完事。
獲取等待隊列的方式:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
四.總結
總而言之,線程池的大概的業務也就可以基本搞清楚了,這裏我們說點題外話。
線程池雖然是個非常優秀的設計,但他並非萬能,並非需要線程的地方就一定要用線程池,有些時候線程池反而會影響正常的功能。
在高運算的時候,可以使用線程池來限制線程的數目同時加快運算,但是在IO阻塞的時候,線程池就不應該被運用了,因爲IO的讀取並不符合線程池設計解決的問題。
當然我們也可以使用純粹的線程IO讀取完畢後將邏輯交給線程池處理,但是在http服務器日趨成熟,websocket被廣泛運用的今天,這種架構的設計也就從與一般的開發人員牽扯不上關係了。但是瞭解底層的運行邏輯,興許會在日常開發中幫助我們。
此外,在線程池中被運用到的,原子類、線程安全的隊列等,我也會在未來的博客中進行解讀。