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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Simplify Java Async Tasks with LatchUtils: A Clean Alternative to CountDownLatch

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.

JavaconcurrencyUtilityAsyncExecutorServiceCountDownLatchLatchUtils
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.