Using CompletableFuture for Asynchronous Programming in Java: Examples and Best Practices
This article explains why asynchronous programming improves performance, compares serial and parallel implementations of a login flow, and demonstrates how to use Java's CompletableFuture API—including supplyAsync, runAsync, callbacks, composition, and error handling—to write efficient backend code.
Most developers perform CRUD operations synchronously; this article shows how to use CompletableFuture to optimize project code and improve performance.
Why use asynchronous programming? In a login scenario, fetching role, menu, balance, and points sequentially takes 1 second (500 ms + 200 ms + 200 ms + 100 ms). Running these tasks in parallel reduces the total time to the longest single task (500 ms), effectively doubling throughput.
Serial implementation example:
@Test
public void login(Long userId) {
log.info("开始查询用户全部信息---串行!");
getUserRole(userId);
getUserMenu(userId);
getUserAmount(userId);
getUserIntegral(userId);
log.info("封装用户信息返回给前端!");
}Asynchronous implementation with CompletableFuture:
@Test
public void asyncLogin() {
long startTime = System.currentTimeMillis();
log.info("开始查询用户角色信息!");
CompletableFuture<Map<String, Object>> roleFuture = CompletableFuture.supplyAsync(() -> {
Thread.sleep(500);
Map<String, Object> map = new HashMap<>();
map.put("role", "管理员");
return map;
});
// similar futures for menu, amount, integral
roleFuture.join();
// join other futures
log.info("查询用户全部信息总耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
}Creating tasks: supplyAsync returns a value, while runAsync does not. Both can use the default ForkJoinPool or a custom ExecutorService.
Callback methods: thenRun / thenRunAsync execute a second task after the first without passing a result; thenAccept / thenAcceptAsync receive the previous result but return void; thenApply / thenApplyAsync transform the result and return a new value.
Example of thenRun:
CompletableFuture<Void> amountFuture = CompletableFuture.runAsync(() -> {
// simulate work
});
CompletableFuture<Void> next = amountFuture.thenRun(() -> {
// second task
});
next.get();Combining multiple futures: thenCombine, thenAcceptBoth, and runAfterBoth wait for two futures to finish (AND logic). applyToEither, acceptEither, and runAfterEither proceed when any one finishes (OR logic). allOf completes when all supplied futures finish; anyOf completes when any finishes.
Error handling: exceptionally handles exceptions and provides a fallback value; whenComplete runs after completion without altering the result; handle can transform the result or recover from errors.
Common pitfalls: Calling Future.get() or join() is required to surface exceptions; get() blocks, so a timeout should be set. The default thread pool size equals CPU cores – 1, which may be insufficient for high‑load services; using a custom thread pool with an appropriate rejection policy (e.g., AbortPolicy) is recommended.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
