1、簡單介紹
我們在併發編程中,目前大部分做法都是將任務添加到線程池中,並拿到Future對象,將其添加到集合中,等所有任務都添加到線程池後,在通過遍歷Future集合,調用future.get()來獲取每個任務的結果,這樣可以使得先添加到線程池的任務先等待其完成,但是並不能保證第一個添加到線程池的任務就是第一個執行完成的,所以會出現這種情況,後面添加到線程池的任務已經完成了,但是還必須要等待第一個任務執行完成並處理結果後才能處理接下來的任務。
如果想要不管添加到線程池的任務的順序,先完成的任務先進行處理,那麼就需要用到ExecutorCompletionService這個工具了。
2、源碼解析
ExecutorCompletionService實現了CompletionService接口。CompletionService接種有有以下方法。
public interface CompletionService<V> {
// 提交任務
Future<V> submit(Callable<V> task);
// 提交任務
Future<V> submit(Runnable task, V result);
// 獲取任務結果,帶拋出異常
Future<V> take() throws InterruptedException;
// 獲取任務結果
Future<V> poll();
// 獲取任務結果,帶超時
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}
可以看到接口中的方法非常簡單,只有提交任務以及獲取任務結果兩類方法。
我們再看下實現類ExecutorCompletionService中的代碼。
public class ExecutorCompletionService<V> implements CompletionService<V> {
private final Executor executor;
private final AbstractExecutorService aes;
private final BlockingQueue<Future<V>> completionQueue;
/**
* FutureTask的子類,重寫FutureTask完成後的done方法
*/
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
// task任務執行完成後將任務放到隊列中
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
private RunnableFuture<V> newTaskFor(Callable<V> task) {
if (aes == null)
return new FutureTask<V>(task);
else
return aes.newTaskFor(task);
}
private RunnableFuture<V> newTaskFor(Runnable task, V result) {
if (aes == null)
return new FutureTask<V>(task, result);
else
return aes.newTaskFor(task, result);
}
/**
* 構造方法,傳入一個線程池,創建一個隊列
*/
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
/**
* 構造方法,傳入線程池和隊列
*/
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue) {
if (executor == null || completionQueue == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
}
// 提交一個task任務,最終將任務封裝成QueueingFuture並由指定的線程池執行
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture(f));
return f;
}
// 提交一個task任務,最終將任務封裝成QueueingFuture並由指定的線程池執行
public Future<V> submit(Runnable task, V result) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task, result);
executor.execute(new QueueingFuture(f));
return f;
}
// 從隊列中獲取執行完成的RunnableFuture對象,take方法會阻塞直到有數據
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
// 從隊列中獲取執行完成的RunnableFuture對象
public Future<V> poll() {
return completionQueue.poll();
}
// 從隊列中獲取執行完成的RunnableFuture對象
public Future<V> poll(long timeout, TimeUnit unit)
throws InterruptedException {
return completionQueue.poll(timeout, unit);
}
}
通過觀察實現類中的代碼,我們可以發現這個方法非常簡單,其原理分爲以下幾步:
1、在構造ExecutorCompletionService對象時,需要傳入給定的線程池或者阻塞隊列。
2、當我們提交任務到ExecutorCompletionService時,會將提交的任務包裝成QueueingFuture對象,然後交由我們指定的線程池來執行。
3、當任務執行完成後,QueueingFuture對象會執行最終的done方法(QueueingFuture對象重新的方法),將RunnableFuture對象添加到指定的阻塞隊列中。
4、我們可以通過poll或者take方法來獲取隊列中的RunnableFuture對象,以便獲取執行結果。
由此可以發現我們獲取到的任務執行結果,與提交到線程池的任務順序是無關的,哪個任務先完成,就會被添加到隊列中,我們就可以先獲取執行結果。
3、使用場景
1、當我們不關注提交到線程池任務順序以及任務執行完成獲取結果的順序時,我們就可以使用ExecutorCompletionService這個來執行任務。以下是示例代碼。
void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException, ExecutionException {
CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
for (Callable<Result> s : solvers) {
ecs.submit(s);
}
int n = solvers.size();
for (int i = 0; i < n; ++i) {
Result r = ecs.take().get();
if (r != null) {
use(r);
}
}
}
2、當多個任務同時執行,我們只需要獲取第一個任務的執行結果,其餘結果不需要關心時,也可以通過ExecutorCompletionService來執行任務。以下是示例代碼。
void solve(Executor e, Collection<Callable<Result>> solvers) throws InterruptedException {
CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
int n = solvers.size();
List<Future<Result>> futures = new ArrayList<Future<Result>>(n);
Result result = null;
try {
for (Callable<Result> s : solvers) {
futures.add(ecs.submit(s));
}
for (int i = 0; i < n; ++i) {
try {
Result r = ecs.take().get();
if (r != null) {
result = r;
break;
}
} catch (ExecutionException ignore) {
}
}
} finally {
for (Future<Result> f : futures) {
f.cancel(true);
}
}
if (result != null) {
use(result);
}
}
4、代碼實踐
在業務上我們有這種場景,我們有一批訂單進行批量更新,每處理完一單,我們都需要維護一下處理進度,保證訂單處理進度實時更新成最新的進度數據,我們此時用到的就是ExecutorCompletionService。
protected void parallelBatchUpdateWaybill(Map<String, LwbMain> lwbMainMap, Map<String, UpdateWaybillTaskDetail> taskDetailMap) {
long start = System.currentTimeMillis();
log.info("{} 並行批量更新訂單開始:{}", traceId, taskNo);
int total = lwbMainMap.size();
BlockingQueue<Future<String>> blockingQueue = new LinkedBlockingQueue<>(total + 2);
ExecutorCompletionService<String> executorCompletionService = new ExecutorCompletionService<>(parallelUpdateWaybillExecutorService, blockingQueue);
for (Map.Entry<String, UpdateWaybillTaskDetail> entry : taskDetailMap.entrySet()) {
String lwbNo = entry.getKey();
LwbMain lwbMain = lwbMainMap.get(lwbNo);
UpdateWaybillTaskDetail taskDetail = entry.getValue();
executorCompletionService.submit(() -> this.updateSingleWaybill(lwbMain, taskDetail), "done");
}
for (int current = 0; current < taskDetailMap.size(); current++) {
try {
executorCompletionService.take().get();
} catch (Exception e) {
log.error("{} 獲取並行批量更新訂單結果異常:{}", traceId, e.getMessage(), e);
} finally {
jimClient.incr(importTaskNo);
}
}
long end = System.currentTimeMillis();
log.info("{} 並行批量更新訂單結束:{},耗時:{}", traceId, taskNo, (end - start));
}