多个 Future 对象代表的异步任务的3种方式及优缺点
在 Java 中,当有多个
Future
对象代表的异步任务都完成后再进行下一步处理,有多种实现方式1. 使用 CountDownLatch
CountDownLatch
是 Java 并发包中的一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。可以为每个 Future
任务关联一个 CountDownLatch
,当每个任务完成时减少计数器,当计数器减为 0 时,表示所有任务都已完成。import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class FutureWithCountDownLatch { public static void main(String[] args) throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(3); int taskCount = 3; CountDownLatch latch = new CountDownLatch(taskCount); List<Future<Integer>> futures = new ArrayList<>(); // 提交多个异步任务 for (int i = 0; i < taskCount; i++) { final int index = i; Future<Integer> future = executor.submit(() -> { try { // 模拟任务执行 Thread.sleep((index + 1) * 1000); System.out.println("Task " + index + " completed."); return index * 10; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return -1; } finally { // 任务完成,减少计数器 latch.countDown(); } }); futures.add(future); } // 等待所有任务完成 latch.await(); // 处理所有任务的结果 for (Future<Integer> future : futures) { try { System.out.println("Result: " + future.get()); } catch (ExecutionException e) { e.printStackTrace(); } } executor.shutdown(); } }
2. 使用 Java 8 的 CompletableFuture
CompletableFuture
是 Java 8 引入的一个强大的异步编程工具,它可以方便地处理多个异步任务的组合和完成情况。可以使用 CompletableFuture.allOf
方法等待所有 CompletableFuture
完成。import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class CompletableFutureExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // 创建多个 CompletableFuture 任务 CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task 1 completed."); return 10; }, executor); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task 2 completed."); return 20; }, executor); CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task 3 completed."); return 30; }, executor); // 创建一个新的 CompletableFuture,当所有任务都完成时触发 CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2, future3); // 当所有任务完成后,处理结果 CompletableFuture<List<Integer>> allResults = allFutures.thenApply(v -> { return List.of(future1.join(), future2.join(), future3.join()); }); // 等待结果并处理 allResults.thenAccept(results -> { for (Integer result : results) { System.out.println("Result: " + result); } }).join(); executor.shutdown(); } }
3. 手动轮询检查
通过循环不断检查每个
Future
的 isDone()
方法,直到所有 Future
都完成。import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class ManualPollingExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<>(); // 提交多个异步任务 for (int i = 0; i < 3; i++) { final int index = i; Future<Integer> future = executor.submit(() -> { try { Thread.sleep((index + 1) * 1000); System.out.println("Task " + index + " completed."); return index * 10; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return -1; } }); futures.add(future); } // 手动轮询检查所有任务是否完成 boolean allDone = false; while (!allDone) { allDone = true; for (Future<Integer> future : futures) { if (!future.isDone()) { allDone = false; break; } } } // 处理所有任务的结果 for (Future<Integer> future : futures) { try { System.out.println("Result: " + future.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } executor.shutdown(); } }
以上三种方法各有优缺点,
CountDownLatch
适用于传统的并发编程场景;CompletableFuture
是 Java 8 及以后版本推荐的方式,提供了更丰富的异步编程功能;手动轮询检查简单直接,但效率较低,不适合大规模任务的场景。在生产代码中,使用 Java 8 引入的 CompletableFuture
通常是最合适的选择,以下从几个方面详细分析它相较于 CountDownLatch
和手动轮询的优势:
代码简洁性与可读性
CompletableFuture
:CompletableFuture
提供了一系列丰富的方法,如thenApply
、thenAccept
、thenCompose
、allOf
等,这些方法可以让你以链式调用的方式来组合和处理异步任务,代码结构清晰,易于理解和维护。例如,使用CompletableFuture.allOf
可以轻松地等待多个异步任务完成,再通过thenApply
或thenAccept
处理结果。
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20); CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2); CompletableFuture<List<Integer>> allResults = allFutures.thenApply(v -> List.of(future1.join(), future2.join()) ); allResults.thenAccept(results -> { for (Integer result : results) { System.out.println("Result: " + result); } });
CountDownLatch
:使用CountDownLatch
需要手动管理计数器的增减,并且在任务完成时需要在finally
块中确保计数器被正确减少,代码相对繁琐。
CountDownLatch latch = new CountDownLatch(2); Future<Integer> future1 = executor.submit(() -> { try { return 10; } finally { latch.countDown(); } }); Future<Integer> future2 = executor.submit(() -> { try { return 20; } finally { latch.countDown(); } }); try { latch.await(); System.out.println("Result 1: " + future1.get()); System.out.println("Result 2: " + future2.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
- 手动轮询:手动轮询的代码需要不断地检查每个
Future
的状态,逻辑较为复杂,并且会占用额外的 CPU 资源,代码可读性较差。
List<Future<Integer>> futures = new ArrayList<>(); // 提交任务并添加到 futures 列表 boolean allDone = false; while (!allDone) { allDone = true; for (Future<Integer> future : futures) { if (!future.isDone()) { allDone = false; break; } } } // 处理结果
错误处理
CompletableFuture
:CompletableFuture
提供了专门的异常处理方法,如exceptionally
、handle
等,可以方便地处理异步任务中抛出的异常,并且可以在链式调用中灵活地处理不同阶段的异常。
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() < 0.5) {
throw new RuntimeException("Something went wrong");
}
return 10;
}).exceptionally(ex -> {
System.out.println("Exception caught: " + ex.getMessage());
return -1;
});
CountDownLatch
:在使用CountDownLatch
时,异常处理需要在每个Future
的任务中手动捕获和处理,并且在等待CountDownLatch
时还需要处理InterruptedException
,代码较为复杂。
Future<Integer> future = executor.submit(() -> {
try {
if (Math.random() < 0.5) {
throw new RuntimeException("Something went wrong");
}
return 10;
} catch (RuntimeException ex) {
// 处理异常
return -1;
}
});
try {
latch.await();
Integer result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- 手动轮询:手动轮询的异常处理同样需要在每个
Future
的任务中手动处理,并且在获取结果时还需要处理ExecutionException
和InterruptedException
,容易出现遗漏。
异步组合与流式处理
CompletableFuture
:CompletableFuture
支持将多个异步任务进行组合,如串行执行、并行执行、分支合并等,还可以进行流式处理,非常适合复杂的异步场景。例如,可以使用thenCompose
实现异步任务的串行执行,使用thenCombine
实现两个异步任务的结果合并。
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = future1.thenCompose(result ->
CompletableFuture.supplyAsync(() -> result * 2)
);
CompletableFuture<Integer> future3 = future1.thenCombine(future2, (r1, r2) -> r1 + r2);
CountDownLatch
:CountDownLatch
主要用于简单的等待多个任务完成的场景,对于复杂的异步任务组合和流式处理支持不足。- 手动轮询:手动轮询更侧重于检查任务的完成状态,无法方便地进行异步任务的组合和流式处理。
性能与资源管理
CompletableFuture
:CompletableFuture
内部使用了ForkJoinPool
等线程池机制,能够更好地管理线程资源,避免线程创建和销毁带来的开销,提高性能。同时,它还支持自定义线程池,以满足不同的性能需求。CountDownLatch
:使用CountDownLatch
时需要手动创建和管理线程池,如果线程池配置不合理,可能会导致资源浪费或性能问题。- 手动轮询:手动轮询会不断地检查
Future
的状态,占用额外的 CPU 资源,尤其是在任务数量较多或检查频率较高时,会对性能产生较大影响。
综上所述,
CompletableFuture
在代码简洁性、错误处理、异步组合、性能和资源管理等方面都具有明显的优势,因此在生产代码中是处理多个 Future
完成后进行下一步操作的首选方案。