Backend Development 14 min read

9 Ways to Implement Asynchronous Programming in Java – From Threads to CompletableFuture

This tutorial enumerates nine Java asynchronous programming techniques—including Thread/Runnable, Executors, custom thread pools, Future/Callable, CompletableFuture, ForkJoinPool, Spring @Async, message queues, and Hutool ThreadUtil—explains their advantages and drawbacks, and provides complete code examples for each method.

macrozheng
macrozheng
macrozheng
9 Ways to Implement Asynchronous Programming in Java – From Threads to CompletableFuture

In daily development we often talk about asynchronous programming, such as sending an email after a user registers successfully. This article lists nine ways to achieve async in Java and provides code demos.

1. Using Thread and Runnable

Thread and Runnable are the most basic asynchronous approach, but direct use is discouraged because of high resource consumption, difficult management, lack of scalability, and thread reuse issues.

<code>public class Test {
    public static void main(String[] args) {
        System.out.println("Main thread:" + Thread.currentThread().getName());
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("Async thread test:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }
}
</code>

Resource consumption : creating a new thread each time consumes system resources and degrades performance.

Hard to manage : manual lifecycle, exception handling, and scheduling are complex.

Lack of scalability : cannot easily control concurrent thread count, may exhaust resources.

Thread reuse problem : each task creates a new thread, resulting in low efficiency.

2. Using Executors thread pool

Thread pools address the drawbacks of raw threads. They manage threads, reduce creation/destruction overhead, improve response speed, and enable reuse.

Manages threads to avoid extra creation/destruction cost.

Improves response speed by reusing existing threads.

Reuse of threads saves resources.

Simple demo:

<code>public class Test {
    public static void main(String[] args) {
        System.out.println("Main thread:" + Thread.currentThread().getName());
        ExecutorService executor = Executors.newFixedThreadPool(3);
        executor.execute(() -> {
            System.out.println("ThreadPool async thread:" + Thread.currentThread().getName());
        });
    }
}
</code>

3. Using custom thread pool

Custom thread pools allow configuring core size, max size, queue capacity, etc., avoiding the unbounded queues of Executors' default pools.

<code>public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // core threads
                4, // max threads
                60, TimeUnit.SECONDS, // keep‑alive time
                new ArrayBlockingQueue<>(8), // task queue
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        System.out.println("Main thread:" + Thread.currentThread().getName());
        executor.execute(() -> {
            try {
                Thread.sleep(500);
                System.out.println("Custom thread pool async:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}
</code>

4. Using Future and Callable

Future and Callable (Java 5) enable asynchronous tasks that return results. Callable can return a value and throw exceptions; Future represents the result.

<code>public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        System.out.println("Main thread:" + Thread.currentThread().getName());
        Callable<String> task = () -> {
            Thread.sleep(1000);
            System.out.println("Custom thread pool async:" + Thread.currentThread().getName());
            return "Hello: async task";
        };
        Future<String> future = executor.submit(task);
        String result = future.get(); // blocks until completion
        System.out.println("Async result: " + result);
    }
}
</code>

5. Using CompletableFuture

Java 8 introduced CompletableFuture, offering powerful async capabilities such as chaining, exception handling, and combining multiple tasks.

<code>public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(8), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        System.out.println("Main thread:" + Thread.currentThread().getName());
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("CompletableFuture async:" + Thread.currentThread().getName());
                return "Hello: async task";
            } catch (InterruptedException e) {
                e.printStackTrace();
                return "Hello: async task";
            }
        }, executor);
        future.thenAccept(result -> System.out.println("Async result: " + result));
        future.join();
    }
}
</code>

6. Using ForkJoinPool

ForkJoinPool (Java 7) is designed for divide‑and‑conquer tasks, providing fork and join operations and work‑stealing.

<code>public class Test {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int result = pool.invoke(new SumTask(1, 100));
        System.out.println("Sum 1‑100 = " + result);
    }
    static class SumTask extends RecursiveTask<Integer> {
        private final int start;
        private final int end;
        SumTask(int start, int end) { this.start = start; this.end = end; }
        @Override
        protected Integer compute() {
            if (end - start <= 10) {
                int sum = 0;
                for (int i = start; i <= end; i++) sum += i;
                return sum;
            } else {
                int mid = (start + end) / 2;
                SumTask left = new SumTask(start, mid);
                SumTask right = new SumTask(mid + 1, end);
                left.fork();
                return right.compute() + left.join();
            }
        }
    }
}
</code>

7. Spring @Async

Spring’s @Async annotation enables asynchronous method execution. Enable async support with @EnableAsync, annotate methods with @Async, and optionally configure a custom thread pool.

<code>@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
    }
}
</code>
<code>@Service
public class TianLuoAsyncService {
    @Async
    public void asyncTianLuoTask() {
        try {
            Thread.sleep(2000);
            System.out.println("Async task completed, thread: " + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
</code>
<code>@Configuration
public class AsyncConfig {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("AsyncThread-");
        executor.initialize();
        return executor;
    }
}
</code>

8. MQ implementation

Message queues provide asynchronous processing, decoupling, and traffic shaping. A typical flow saves user data then sends a registration message, while a consumer reads the message and sends notifications.

<code>public void registerUser(String username, String email, String phoneNumber) {
    userService.add(buildUser(username, email, phoneNumber));
    String registrationMessage = "User " + username + " has registered successfully.";
    rabbitTemplate.convertAndSend("registrationQueue", registrationMessage);
}
</code>
<code>@Service
public class NotificationService {
    @RabbitListener(queues = "registrationQueue")
    public void handleRegistrationNotification(String message) {
        System.out.println("Sending registration notification: " + message);
        sendSms(message);
        sendEmail(message);
    }
}
</code>

9. Hutool ThreadUtil

Hutool’s ThreadUtil offers convenient thread‑pool management and async task scheduling.

<code>public class Test {
    public static void main(String[] args) {
        System.out.println("Main thread");
        ThreadUtil.execAsync(() -> {
            System.out.println("Async test: " + Thread.currentThread().getName());
        });
    }
}
</code>
JavaSpringthreadpoolasynchronousCompletableFutureMQ
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login 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.