概述
jdk7新增了併發框架-fork/join框架,在這種框架下,ForkJoinTask代表一個需要執行的任務,真正執行這些任務的線程是放在一個線程池(ForkJoinPool)裏面。ForkJoinPool是一個可以執行ForkJoinTask的ExcuteService,與ExcuteService不同的是它採用了work-stealing模式:所有在池中的線程嘗試去執行其他線程創建的子任務,這樣就很少有線程處於空閒狀態,非常高效。
池中維護着ForkJoinWorkerThread對象數組,數組大小由parallelism屬性決定,parallelism默認爲處理器個數
int n = parallelism << 1;
if (n >= MAX_ID)
n = MAX_ID;//MAX_ID=0x7fff
else {
n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8;
}
workers = new ForkJoinWorkerThread[n + 1];
可見線程個數不會超過0x7fff。
添加線程
什麼情況下需要添加線程呢?當新的任務到來,線程池會通知其他線程前去處理,如果這時沒有處於等待的線程或者處於活動的線程非常少(這是通過ctl屬性來判斷的),就會往線程池中添加線程。
private void addWorker() {
Throwable ex = null;
ForkJoinWorkerThread t = null;
try {
t = factory.newThread(this);
} catch (Throwable e) {
ex = e;
}
if (t == null) { // null or exceptional factory return
long c; // adjust counts
do {} while (!UNSAFE.compareAndSwapLong
(this, ctlOffset, c = ctl,
(((c - AC_UNIT) & AC_MASK) |
((c - TC_UNIT) & TC_MASK) |
(c & ~(AC_MASK|TC_MASK)))));
// Propagate exception if originating from an external caller
if (!tryTerminate(false) && ex != null &&
!(Thread.currentThread() instanceof ForkJoinWorkerThread))
UNSAFE.throwException(ex);
}
else
t.start();
}
添加線程的代碼比較簡單,通過工廠類創建一個線程,通過調用ForkJoinWorkerThread的run方法啓動這個線程。如果失敗則恢復ctl以前的值,並終止線程。工廠類直接調用其構造方法,最終添加線程其實是在registerWorker方法完成的
for (int g;;) {
ForkJoinWorkerThread[] ws;
if (((g = scanGuard) & SG_UNIT) == 0 &&
UNSAFE.compareAndSwapInt(this, scanGuardOffset,
g, g | SG_UNIT)) {
int k = nextWorkerIndex;
try {
if ((ws = workers) != null) { // ignore on shutdown
int n = ws.length;
if (k < 0 || k >= n || ws[k] != null) {
for (k = 0; k < n && ws[k] != null; ++k)
;
if (k == n)
ws = workers = Arrays.copyOf(ws, n << 1);
}
ws[k] = w;
nextWorkerIndex = k + 1;
int m = g & SMASK;
g = (k > m) ? ((m << 1) + 1) & SMASK : g + (SG_UNIT<<1);
}
} finally {
scanGuard = g;
}
return k;
}
else if ((ws = workers) != null) { // help release others
for (ForkJoinWorkerThread u : ws) {
if (u != null && u.queueBase != u.queueTop) {
if (tryReleaseWaiter())
break;
}
}
}
這裏有個屬性scanGuard有必要提一下,從Guard這個詞可以知道它是在保護什麼,是在保護works這個數組。當需要更新這個數組時,通過不斷地檢查scanGuard來達到保護的目的。
整個框架大量採用順序鎖,好處是不用阻塞,不好的地方是會有額外的循環。這裏也是通過循環來註冊這個線程,在循環的過程中有兩種情況發生:1、compareAndSwapInt操作成功;2、操作失敗。第一種情況:掃描workers數組,找到一個爲空的項,並把新創建的線程放在這個位置;如果沒有找到,表示數組大小不夠,則將數組擴大2倍;
第二種情況:需要循環重新嘗試直接成功爲止,從代碼中可以看出,即使是失敗了,也不忘做一些額外的事:通知其他線程去執行沒有完成的任務。
執行任務
public void execute(ForkJoinTask<?> task) {
if (task == null)
throw new NullPointerException();
forkOrSubmit(task);
}
public void execute(Runnable task) {
if (task == null)
throw new NullPointerException();
ForkJoinTask<?> job;
if (task instanceof ForkJoinTask<?>) // avoid re-wrap
job = (ForkJoinTask<?>) task;
else
job = ForkJoinTask.adapt(task, null);
forkOrSubmit(job);
}
對參數爲Runnable的execute進行了加強,將Runnable這種普通任務適配成ForkJoinTask這種任務,然後做爲參數傳給forkOrSubmit方法統一處理。 private <T> void forkOrSubmit(ForkJoinTask<T> task) {
ForkJoinWorkerThread w;
Thread t = Thread.currentThread();
if (shutdown)
throw new RejectedExecutionException();
if ((t instanceof ForkJoinWorkerThread) &&
(w = (ForkJoinWorkerThread)t).pool == this)
w.pushTask(task);
else
addSubmission(task);
}
從以上代碼可以看出,這兩種任務最終的歸屬還是不一樣,ForkJoinTask這種任務被放到線程內部的隊列裏面,普通的Runnable任務被放到線程池的隊列裏面了。終止線程池
和ExecutorService一樣,可以調用shutdown()和 shutdownNow()來終止線程,會先設置每個線程的任務狀態爲CANCELLED,然後調用Thread的interrupt方法來終止每個線程。
總結:ForkJoinPool就是一個ExcuteService,與ExcuteService不同的是:
1、ExcuteService執行Runnable/Callable任務,ForkJoinPool除了可以執行Runnable任務外,還可以執行ForkJoinTask任務;
2、ExcuteService中處於後面的任務需要等待前面任務執行後纔有機會執行,而ForkJoinPool會採用work-stealing模式幫助其他線程執行任務,即ExcuteService解決的是併發問題,而ForkJoinPool解決的是並行問題。
下一節分析Fork/Join框架中的ForkJoinTask任務