ExecutorService中定義了兩個批量執行任務的方法,invokeAll()和invokeAny(),在批量執行或多選一的業務場景中非常方便。invokeAll()在所有任務都完成(包括成功/被中斷/超時)後才會返回,invokeAny()在任意一個任務成功(或ExecutorService被中斷/超時)後就會返回。
AbstractExecutorService實現了這兩個方法,本文將先後分析invokeAll()和invokeAny()兩個方法的原始碼實現。
JDK版本:oracle java 1.8.0_102
invokeAll()
invokeAll()在所有任務都完成(包括成功/被中斷/超時)後才會返回。有不限時和限時版本,從更簡單的不限時版入手。
不限時版
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks) {
RunnableFuture<T> f = newTaskFor(t);
futures.add(f);
execute(f);
}
for (int i = 0, size = futures.size(); i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
try {
f.get(); // 無所謂先執行哪個任務的get()方法
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
}
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}
複製程式碼
8-12行,先將所有任務都提交到執行緒池(當然,任何ExecutorService均可)中。
嚴格來說,不是“提交”,而是“執行”。執行可能是同步或非同步的,取決於執行緒池的策略。不過由於我們僅討論非同步情況(同步同理),用“提交”一詞更容易理解。下同。
13-22行,for迴圈的目的是阻塞呼叫invokeAll的執行緒,直到所有任務都執行完畢。當然我們也可以使用其他方式實現阻塞,不過這種方式是最簡單的:
- 15行如果f.isDone()返回true,則當前任務已結束,繼續檢查下一個任務;否則,呼叫f.get()讓執行緒阻塞,直到當前任務結束。
- 17行無所謂先執行哪一個FutureTask例項的get()方法。由於所有任務併發執行,總體阻塞時間取決於於是耗時最長的任務,從而實現了invodeAll的阻塞呼叫。
- 18-20行沒有捕獲InterruptedException。如果有任務被中斷,主執行緒將丟擲InterruptedException,以響應中斷。
最後,為防止在全部任務結束之前過早退出,23行、25-29行相配合,如果done不為true(未執行到40行就退出了)則取消全部任務。
限時版
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
long nanos = unit.toNanos(timeout);
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
boolean done = false;
try {
for (Callable<T> t : tasks)
futures.add(newTaskFor(t));
final long deadline = System.nanoTime() + nanos;
final int size = futures.size();
// Interleave time checks and calls to execute in case
// executor doesn't have any/much parallelism.
for (int i = 0; i < size; i++) {
execute((Runnable)futures.get(i));
nanos = deadline - System.nanoTime();
if (nanos <= 0L) // 及時檢查是否超時
return futures;
}
for (int i = 0; i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
if (nanos <= 0L) // 及時檢查是否超時
return futures;
try {
f.get(nanos, TimeUnit.NANOSECONDS);
} catch (CancellationException ignore) {
} catch (ExecutionException ignore) {
} catch (TimeoutException toe) {
return futures;
}
nanos = deadline - System.nanoTime();
}
}
done = true;
return futures;
} finally {
if (!done)
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}
複製程式碼
10-11行,先將所有任務封裝為FutureTask,新增到futures列表中。
18-23行,每提交一個任務,就立刻判斷是否超時。這樣的話,如果在任務全部提交到執行緒池中之前,就已經達到了超時時間,則能夠儘快檢查出超時,結束提交併退出。
對於限時版,將封裝任務與提交任務拆開是必要的。
28-29行,每次在呼叫限時版f.get()進入阻塞狀態之前,先檢查是否超時。這裡也是希望超時後,能夠儘快發現並退出。
其他同不限時版。
invokeAny()
invokeAny()在任意一個任務成功(或ExecutorService被中斷/超時)後就會返回。也分為不限時和限時版本,但為了進一步保障效能,invokeAny()的實現思路與invokeAll()略有不同。
public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException {
try {
return doInvokeAny(tasks, false, 0);
} catch (TimeoutException cannotHappen) {
assert false;
return null;
}
}
public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return doInvokeAny(tasks, true, unit.toNanos(timeout));
}
複製程式碼
內部呼叫了doInvokeAny()。
學習5-8行的寫法,程式碼自解釋。
doInvokeAny()
簡化如下:
private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
boolean timed, long nanos)
throws InterruptedException, ExecutionException, TimeoutException {
...
ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
ExecutorCompletionService<T> ecs =
new ExecutorCompletionService<T>(this);
...
try {
ExecutionException ee = null;
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Iterator<? extends Callable<T>> it = tasks.iterator();
futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1;
for (;;) {
Future<T> f = ecs.poll();
if (f == null) {
if (ntasks > 0) {
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
}
else if (active == 0)
break;
else if (timed) {
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
else
f = ecs.take();
}
if (f != null) {
--active;
try {
return f.get();
} catch (...) {
ee = ...;
}
}
}
...
throw ee;
} finally {
for (int i = 0, size = futures.size(); i < size; i++)
futures.get(i).cancel(true);
}
}
複製程式碼
要點:
- ntasks維護未提交的任務數,active維護已提交未結束的任務數。
- 內部使用ExecutorCompletionService維護已完成的任務。
- 如果沒有任務成功結束,則返回捕獲的最後一個異常。
- 第一個任務是必將被執行的,其他任務按照迭代器順序增量提交。
增量提交有什麼好處呢?節省資源,如果在提交過程中就有任務完成了,那麼沒必要繼續提交任務耗費時間和空間;降低延遲,如果有任務完成,與全量提交相比,能更早被發現。
14行先向執行緒池提交一個任務(迭代器第一個),ntasks--,active=1:
futures.add(ecs.submit(it.next()));
--ntasks;
int active = 1;
複製程式碼
這裡是真“提交”了,不是“執行”。
然後18-45行迴圈檢查是否有任務成功結束。
首先,19行通過及時返回的poll()方法,嘗試取出一個已完成的任務:
Future<T> f = ecs.poll();
複製程式碼
根據f的結果,分成兩種情況討論。
ExecutorCompletionService預設使用LinkedBlockingQueue作為任務佇列。對LinkedBlockingQueue不熟悉的可參照原始碼|併發一枝花之BlockingQueue。
case1:如果有任務完成
如果有任務完成,則f不為null,進入40-49行,active--,並嘗試取出任務結果:
if (f != null) {
--active;
try {
return f.get();
} catch (...) {
ee = ...;
}
}
複製程式碼
- 如果能夠成功取出,即當前任務已成功結束,直接返回。
- 如果丟擲異常,則當前任務異常結束,使用ee記錄異常。
顯然,如果已完成的任務是異常結束的,invokeAny()不會退出,而是繼續檢視其它任務。
FutureTask#get()的用法參照原始碼|使用FutureTask的正確姿勢。
case2:如果沒有任務完成
如果沒有任務完成,則f為null,進入23-39行,判斷是繼續提交任務、退出還是等待任務結果:
if (f == null) {
if (ntasks > 0) { // check1
--ntasks;
futures.add(ecs.submit(it.next()));
++active;
}
else if (active == 0) // check2
break;
else if (timed) { // check3
f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
if (f == null)
throw new TimeoutException();
nanos = deadline - System.nanoTime();
}
else // check4
f = ecs.take();
}
複製程式碼
- check1:如果還有剩餘任務(ntasks > 0),那就繼續提交,同時ntasks--,active++。
- check2:如果沒有剩餘任務了,且也沒有已提交未結束的任務(active == 0),則表示全部任務均已執行結束,但沒有一個任務是成功的,可以退出迴圈。退出迴圈後,將在47行丟擲ee記錄的最後一個異常。
- check3:如果可以沒有剩餘任務,但還有已提交未結束的任務,且開啟了超時機制,則嘗試使用超時版poll()等待任務完成。但是,如果這種情況下超時了,就表示整個invokeAny()方法超時了,所以poll()返回null的時候,要主動丟擲TimeoutException。
- check4:如果可以沒有剩餘任務,但還有已提交未結束的任務,且未開啟超時機制,則使用無限阻塞的take()方法,等待任務完成。
這種一堆if-else的程式碼很醜。可修改如下:
if (f == null) { // check1 if (ntasks > 0) { --ntasks; futures.add(ecs.submit(it.next())); ++active; continue; } if (active == 0) { // check2 assert ntasks == 0; // 防止自己改吧改吧把它這句判斷挪到了前面 break; } if (timed) { // check3 f = ecs.poll(nanos, TimeUnit.NANOSECONDS); if (f == null) { throw new TimeoutException(); } nanos = deadline - System.nanoTime(); } else { // check4 f = ecs.take(); } } 複製程式碼
修改依據:
- check1、check2、check3/check4沒有並列的判斷關係
- check3、check4有並列的判斷關係,非此即彼
- 結構更清爽
總結
不會寫總結。。。
但是會寫吐槽啊!!!
猴子現在每次寫部落格都經歷著從“臥槽似乎很簡單啊,寫個毛”到“臥槽這跟想象的不一樣啊!臥槽巨帥!”的心態崩塌,各位巨巨寫的程式碼是真好看,效能還棒棒噠,羨慕崇拜打雞血。哎,,,保持謙卑,並羨慕臉遙拜各位巨巨。
本文連結:原始碼|批量執行invokeAll()&&多選一invokeAny()
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。