Java容器
在Java中,容器共有2大類,一種是Collection,另一種是map。其中Collection下又有2中集合類型,一種是普通存儲數據類的list以及set,還有一種是爲高併發服務的queue。爲什麼要提一下容器呢,因爲我們下面提到的線程池將會用到這裏的queue。
線程池
在Java中有兩種線程池
- ThreadPoolExecutor
自定義線程池,可以定義線程數量,等待隊列,拒絕策略等 - ForkJoinPool
Fork/Join框架是Java 7提供的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。
主要的區別
- 使用場景不同,ThreadPoolExecutor可以自定義任務,ForkJoinPool適合具有父子關係的任務,適合分治算法
- 使用方法不同,ThreadPoolExecutor需要自定義線程池大小,等待隊列具體類型(ArrayQueue需要指定queue長度,LinkQueue長度則爲int的最大值),ForkJoinPool線程數量默認是CPU的核心數,等待隊列近乎無限,有造成OOM的風險
後面線程池以ThreadPoolExecutor爲基數進行討論。
Executor、Future、FutureTask
Executor
Executor接口,是線程池的根接口:
下面我們來看下ThreadPoolExecutor的類關係圖:
從第一幅截圖中我們可以看到Executor接口中定義了一個execute方法,看方法名也很好理解,用來執行task,同時返回值類型爲void。
下面我們簡單看下其子接口ExecutorService中又定義了哪些內容:
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
很好理解,定義了一些shutdown方法,以及執行單個task,多個task的方法。
我們以submit爲例:
我們一定可以看到方法的入參是Callable,或者Runnable,其方法返回值是個Future,這都啥玩樣兒?
不急,我們看源碼。
首先我們看下Runnable:
public interface Runnable {
public abstract void run();
}
再看下Callable:
public interface Callable<V> {
V call() throws Exception;
}
Future
至此,我們就可以弄清楚了,Runnable執行run方法,沒有返回值,Callable執行的是call方法,這個是有返回值的,也就是說通過call方法,我們就可以拿到線程執行task的結果了。放到線程池中,對應的返回值類型就是個Future,那這個Future又是個啥呢?
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
根據接口定義,我們可以很清楚的知道了,Future封裝了thread的運行狀態,以及thread運行後的返回值,通過get方法,就可以拿到線程的執行結果。但是,請注意,這個get方法是阻塞的,如果線程內部有死循環等問題,線程將會一直堵塞下去,風險係數非常高。
建議使用其子類CompletableFuture,JDK已經封裝了很多方便的異步邏輯,具體使用線程池這部分就不多做介紹了,後面有機會再寫。
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
private Callable<V> callable;
private Object outcome; // non-volatile, protected by state reads/writes
private volatile Thread runner;
private volatile WaitNode waiters;
.
.
.
}
摘抄部分,可以看到FutureTask即實現了接口RunnableFuture,又持有Callable對象,可以認爲FutureTask同時擁有Runnable和Callable特性。
ThreadPoolExecutor
終於講到ThreadPoolExecutor了,我們先看到pool的構造器,以參數最全的那個爲例:
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:核心線程數,這部分的線程就算空閒,也不會歸還給os
maximumPoolSize:最大線程數,當任務量太多,核心線程處理不過來的時候,會動態擴容到的最大線程數量
keepAliveTime:線程最大空閒時間,空閒超過該時間,該線程歸還給os
unit:時間單位
workQueue:線程阻塞queue,用來緩存暫時處理不過來的任務
threadFactory:線程工廠,用來定義以什麼樣的方式創建線程
handler:拒絕策略,當任務超過最大線程數之後,對後續任務怎麼做處理。jdk自帶4種策略,建議自定義。
執行過程
假設我們現在已經創建出了一個線程池,核心線程數2,最大線程數4,任務隊列大小2。現在有7個task需要被執行,過程如下:
- task1:此時線程池內沒有線程,首先創建出一個線程thread1來執行task1
- task2:核心線程數爲2,現有線程數爲1,沒有超過設定值,創建線程thread2來執行task2
- task3:核心線程數2,現有線程數2,已經達到設定值,且等待隊列爲空,將task3放入等待隊列
- task4:與task3類似,等待隊列沒有滿,將task4放入等待隊列
- task5:核心線程數已滿,等待隊列已滿,但是最大線程數爲4,現有線程爲2,則創建線程thread3執行task5
- task6:與task5類似,還沒有達到最大線程數,創建thread4執行task6
- task7:核心線程已滿,最大線程已滿,等待隊列已滿,線程池無法消化該任務,執行拒絕策略。
實現原理
我們以submit爲例,探討下線程池工作原理:
首先是入口方法:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
首先先將要執行的task封裝爲RunnableFuture對象ftask(實際是new了一個FutureTask),這個對象本身是Runnable接口的實例,所以可以直接將ftask扔給execute方法,執行task,同時將ftask返回(即實際返回類型是個FutureTask)。
下面再看下execute方法:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //最大線程數 2^29 -1
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;
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
從上面的源碼中可以看出,線程池的狀態以及數量,都跟COUNT_BITS這個變量有關係:
- COUNT_BITS:使用了int 32位的後29位,
- RUNNING: -1 << COUNT_BITS
-1在Java底層是由32個1表示的,左移29位的話,即111 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位全部爲1的話,表示RUNNING狀態,即-536870912; - SHUTDOWN = 0 << COUNT_BITS
0在Java底層是由32個0表示的,無論左移多少位,還是32個0,即000 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位全部爲0的話,表示SHUTDOWN狀態,即0; - STOP = 1 << COUNT_BITS
1在Java底層是由前面的31個0和1個1組成的,左移29位的話,即001 00000 00000000 00000000 00000000,也就是低29位全部爲0,高3位爲001的話,表示STOP狀態,即536870912; - 後面還有兩個也類似,就不寫了
從上面的解釋我們便可以看出,ctl的前3位表示的是當前線程池的運行狀態,後29位代表是當前線程池中活動線程的數量,每當新增或者減少,只需要改動後29位中的值就可以了。
當有了這層理解之後,再看下面的execute方法就很容易了:
- 獲取當前的ctl
- 根據ctl的後29位,判斷當前線程數是否小於核心線程數,如果是,執行addWorker,直接返回
- 如果核心線程都被佔滿了,判斷當前線程池是否是running狀態,如果是,則通過offer方法,將當前任務添加至任務隊列
- 再次嘗試執行當前task,從緩存queue中取出該task,如果當前核心線程還是被佔着,則調用addWorker,傳入false參數,表示創建非核心線程
- 如果在第三步的時候,queue已經滿了,無法添加,則直接調用addWorker啓動非核心線程執行,如果執行失敗,則執行拒絕策略。
所以,任務運行的核心就在於addWorker方法,我們一起來看下其源碼,代碼比較多,主要有兩塊內容,我們先看其中的第一部分:
private boolean addWorker(Runnable firstTask, boolean core) {
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
}
}
// 後面是第二部分,等下再講
.
.
.
}
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
通過上面的解釋,我們大概可以知道addWorker方法的功能,即在線程池中添加線程來執行,這一部分的內容寫了兩層自旋,在多線程環境下,通過一層層的狀態判斷,最終目的就是爲了執行compareAndIncrementWorkerCount方法,也就是在後29位的數字基礎上+1,表示當前活躍的線程數量+1。
也就是,只有在判斷線程池中能夠創建新線程,之後,才能去執行task,這個也很好理解。
下面看第二部分:
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);
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;
}
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
主要的邏輯有下面幾步:
- 將當前任務封裝爲一個Worker,怎麼封裝的呢,見Worker的構造器,很簡單,在創建線程池的時候,有一個參數代表的是如何創建thread的ThreadFactory,創建出一個線程之後,將當前的任務綁定到該thread
- 執行:workers.add(w); 將新建的worker放入workers集合中去
- 添加成功之後,執行:t.start();執行當前worker綁定的線程
到此處,我們往線程池中添加任務,線程池執行任務,就完成了。
線程複用
等等,似乎少了點什麼,是什麼呢?
我們回顧下上面的過程,在上面的過程中,線程總是被ThreadFactory創建出來,然後綁定task,然後在執行task。
是不是跟我們鎖理解的線程池有點不一樣呢?我們使用線程池的目的就是爲了實現線程的複用,而目前看下來,線程池所做的事情,是在不停的new線程,什麼時候複用的呢??
是不是很困惑呢?
我們知道,Thread.start()只能調用一次,一旦這個調用結束,則該線程就到了stop狀態,不能再次調用start。
所以要實現複用,只能是在run方法裏面做文章了,線程池是怎麼做的呢?
public void run() {
runWorker(this);
}
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);
}
}
由此我們可以看到,線程的run實際最終執行的是runWorker方法,看到了嗎,while 循環!是不是很興奮?
沒錯,當線程創建出來之後,如果當前線程上有綁定task,則執行當前task,如果沒有綁定task,則根據getTask方法去獲取task,然後執行task。
再追蹤下getTask方法:
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;
}
}
}
我們看到了什麼?workQueue.poll !!任務隊列啊,簡直熱淚盈眶。
至此,我們便弄清楚了線程池是如何創建線程以及線程複用的。
- 如果線程池爲空,或者活躍的線程數量<核心線程數,或者緩存隊列已滿,但活躍線程數量沒有超過最大線程數時,創建新線程,在創建線程的過程中,綁定當前的task,並執行
- 當前task執行完畢之後,該線程並沒有退出,而是再次從任務緩存列隊中取出第一條task繼續執行,只要能獲取到獲取到任何一條task,當前Thread都不會退出,會一直執行下去