Simplify Java Async Tasks with LatchUtils: A Clean Alternative to CountDownLatch
This article introduces LatchUtils, a lightweight Java utility that streamlines asynchronous task registration and waiting, compares it with manual CountDownLatch and CompletableFuture approaches, and demonstrates how it reduces boilerplate while keeping business logic clear and maintainable.
In Java application development, asynchronous parallel processing is often needed for time‑consuming operations such as external API calls, database queries or heavy calculations. When the main flow must wait for all parallel tasks to finish, developers usually reach for ExecutorService, CountDownLatch and other concurrency utilities.
Using these raw tools, however, leads to repetitive “glue” code that obscures business logic. To address this, a lightweight helper class named LatchUtils is introduced. It provides a concise way to register asynchronous tasks and wait for their completion with a single call.
Detailed Code
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
public class LatchUtils {
private static final ThreadLocal<List<TaskInfo>> THREADLOCAL = ThreadLocal.withInitial(LinkedList::new);
public static void submitTask(Executor executor, Runnable runnable) {
THREADLOCAL.get().add(new TaskInfo(executor, runnable));
}
private static List<TaskInfo> popTask() {
List<TaskInfo> taskInfos = THREADLOCAL.get();
THREADLOCAL.remove();
return taskInfos;
}
public static boolean waitFor(long timeout, TimeUnit timeUnit) {
List<TaskInfo> taskInfos = popTask();
if (taskInfos.isEmpty()) {
return true;
}
CountDownLatch latch = new CountDownLatch(taskInfos.size());
for (TaskInfo taskInfo : taskInfos) {
Executor executor = taskInfo.executor;
Runnable runnable = taskInfo.runnable;
executor.execute(() -> {
try {
runnable.run();
} finally {
latch.countDown();
}
});
}
boolean await = false;
try {
await = latch.await(timeout, timeUnit);
} catch (Exception ignored) {
}
return await;
}
private static final class TaskInfo {
private final Executor executor;
private final Runnable runnable;
public TaskInfo(Executor executor, Runnable runnable) {
this.executor = executor;
this.runnable = runnable;
}
}
}Core Idea
The design philosophy of LatchUtils is “multiple submissions, single wait”.
Task registration: In the main flow call LatchUtils.submitTask(executor, runnable) to register a Runnable and its Executor.
Execute and wait: After all tasks are registered, invoke LatchUtils.waitFor(timeout, timeUnit). This method triggers execution of all registered tasks and blocks until they finish or the timeout expires.
API Overview
submitTask()
Function: Submit an asynchronous task.
Parameters: java.util.concurrent.Executor executor – the thread pool that runs the task. java.lang.Runnable runnable – the business logic to execute asynchronously.
waitFor()
Function: Trigger execution of all submitted tasks and synchronously wait for them to complete.
Parameters: long timeout – maximum waiting time. java.util.concurrent.TimeUnit timeUnit – time unit of the timeout.
Return value: true if all tasks finish within the timeout, false otherwise. The method automatically clears the task list, allowing reuse.
Practical Example
A typical scenario where an aggregation service calls user, order and product services in parallel and proceeds after all results are obtained.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
System.out.println("主流程开始,准备分发异步任务...");
// Task 1: fetch user info
LatchUtils.submitTask(executorService, () -> {
try {
System.out.println("开始获取用户信息...");
Thread.sleep(1000);
System.out.println("获取用户信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Task 2: fetch order info
LatchUtils.submitTask(executorService, () -> {
try {
System.out.println("开始获取订单信息...");
Thread.sleep(1500);
System.out.println("获取订单信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Task 3: fetch product info
LatchUtils.submitTask(executorService, () -> {
try {
System.out.println("开始获取商品信息...");
Thread.sleep(500);
System.out.println("获取商品信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println("所有异步任务已提交,主线程开始等待...");
boolean allTasksCompleted = LatchUtils.waitFor(5, TimeUnit.SECONDS);
if (allTasksCompleted) {
System.out.println("所有异步任务执行成功,主流程继续...");
} else {
System.err.println("有任务执行超时,主流程中断!");
}
executorService.shutdown();
}
}Output shows that the main flow only needs to submit tasks and wait, while the underlying CountDownLatch handling is hidden.
Comparison with Manual CountDownLatch
Manual use of CountDownLatch requires explicit creation, countDown calls inside each task, and error handling, resulting in more boilerplate.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ManualCountDownLatchExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
CountDownLatch latch = new CountDownLatch(3);
System.out.println("主流程开始,准备分发异步任务...");
// Task 1
executorService.execute(() -> {
try {
System.out.println("开始获取用户信息...");
Thread.sleep(1000);
System.out.println("获取用户信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
// Task 2
executorService.execute(() -> {
try {
System.out.println("开始获取订单信息...");
Thread.sleep(1500);
System.out.println("获取订单信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
// Task 3
executorService.execute(() -> {
try {
System.out.println("开始获取商品信息...");
Thread.sleep(500);
System.out.println("获取商品信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
System.out.println("所有异步任务已提交,主线程开始等待...");
boolean allTasksCompleted = false;
try {
allTasksCompleted = latch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主线程在等待时被中断!");
}
if (allTasksCompleted) {
System.out.println("所有异步任务执行成功,主流程继续...");
} else {
System.err.println("有任务执行超时,主流程中断!");
}
executorService.shutdown();
}
}Comparison with CompletableFuture
Using CompletableFuture also reduces boilerplate but still requires creating multiple Future objects and handling several checked exceptions.
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 executorService = Executors.newFixedThreadPool(3);
System.out.println("主流程开始,准备分发异步任务...");
CompletableFuture<Void> userFuture = CompletableFuture.runAsync(() -> {
try {
System.out.println("开始获取用户信息...");
Thread.sleep(1000);
System.out.println("获取用户信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, executorService);
CompletableFuture<Void> orderFuture = CompletableFuture.runAsync(() -> {
try {
System.out.println("开始获取订单信息...");
Thread.sleep(1500);
System.out.println("获取订单信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, executorService);
CompletableFuture<Void> productFuture = CompletableFuture.runAsync(() -> {
try {
System.out.println("开始获取商品信息...");
Thread.sleep(500);
System.out.println("获取商品信息成功!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, executorService);
System.out.println("所有异步任务已提交,主线程开始等待...");
CompletableFuture<Void> allFutures = CompletableFuture.allOf(userFuture, orderFuture, productFuture);
try {
allFutures.get(5, TimeUnit.SECONDS);
System.out.println("所有异步任务执行成功,主流程继续...");
} catch (Exception e) {
System.err.println("任务执行超时或出错,主流程中断! " + e.getMessage());
}
executorService.shutdown();
}
}Comparison Analysis
Feature
LatchUtils
Manual CountDownLatch
CompletableFuture.allOf
Code conciseness
Very high
– business logic and concurrency are separated.
Medium
– countDown logic is scattered.
High
– chainable but multiple Futures needed.
Status management
Automatic
Manual
Automatic
Error handling
Simplified
– waitFor returns a boolean.
Complex
– explicit countDown and InterruptedException handling.
Complex
– get() throws several checked exceptions.
Separation of concerns
Excellent
– only “submit” and “wait”.
Average
– concurrency logic intrudes into business code.
Good
– task definition is separate but composition still required.
Ease of use
Very simple
– minimal learning curve.
Requires CountDownLatch knowledge
Requires CompletableFuture knowledge
Conclusion
For the common pattern “dispatch a set of parallel tasks and wait for all to complete”, LatchUtils dramatically simplifies development by encapsulating concurrency control, keeping business code clean, and improving readability and maintainability.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
