Backend Development 15 min read

Nine Ways to Implement Asynchronous Programming in Java

This article introduces nine different approaches to achieve asynchronous programming in Java, including using Thread and Runnable, Executors thread pools, custom thread pools, Future and Callable, CompletableFuture, ForkJoinPool, Spring @Async, message queues, and Hutool's ThreadUtil, with code examples and usage tips.

IT Services Circle
IT Services Circle
IT Services Circle
Nine Ways to Implement Asynchronous Programming in Java

Preface – In daily development we often need asynchronous programming, for example sending an email after a user registers successfully. This article lists nine ways to implement asynchronous programming in Java.

Using Thread and Runnable

Using Executors thread pools

Using custom thread pools

Using Future and Callable

Using CompletableFuture

Using ForkJoinPool

Spring @Async

Message‑Queue (MQ) implementation

Using Hutool's ThreadUtil

1. Using Thread and Runnable

Thread and Runnable are the most basic asynchronous programming mechanisms. Directly creating a Thread with a Runnable lets you run code in a separate thread.

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

However, in production this approach is discouraged because:

High resource consumption – each new thread consumes system resources.

Difficult management – manual lifecycle, exception handling, and scheduling are complex.

Lack of scalability – cannot easily control the number of concurrent threads.

Thread reuse problems – each task creates a new thread, leading to low efficiency.

2. Using Executors Thread Pools

To avoid the drawbacks of raw threads, we can use the thread‑pool utilities provided by Executors . A pool manages a fixed number of threads, reusing them for multiple tasks.

Manages threads for you, reducing creation and destruction overhead.

Improves response speed because a ready thread is taken from the pool.

Enables reuse of threads, saving resources.

Typical factory methods:

- Executors.newFixedThreadPool
- Executors.newCachedThreadPool

Simple demo:

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

3. Using Custom Thread Pools

While the default pools are convenient, newFixedThreadPool uses an unbounded LinkedBlockingQueue . If tasks are submitted faster than they are processed, the queue can grow indefinitely and exhaust memory.

newFixedThreadPool defaults to an unbounded queue (capacity Integer.MAX_VALUE ). Excessive task accumulation may lead to out‑of‑memory errors.

Therefore we usually create a custom ThreadPoolExecutor with bounded queue and explicit core/max sizes:

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

4. Using Future and Callable

If you need the asynchronous task to return a result, use Callable together with Future . Callable is similar to Runnable but can return a value and throw checked exceptions.

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("TianLuo main thread:" + Thread.currentThread().getName());
        Callable
task = () -> {
            Thread.sleep(1000);
            System.out.println("TianLuo custom pool async:" + Thread.currentThread().getName());
            return "Hello, public account: JianTianLuo!";
        };
        Future
future = executor.submit(task);
        String result = future.get(); // blocks until completion
        System.out.println("Async result: " + result);
    }
}
// Output:
// TianLuo main thread:main
// TianLuo custom pool async:pool-1-thread-1
// Async result: Hello, public account: JianTianLuo!

5. Using CompletableFuture

CompletableFuture (Java 8) offers a richer API for asynchronous programming, supporting chaining, exception handling, and composition of multiple async tasks.

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("TianLuo main thread:" + Thread.currentThread().getName());
        CompletableFuture
future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
                System.out.println("TianLuo CompletableFuture async:" + Thread.currentThread().getName());
                return "Hello, public account: JianTianLuo!";
            } catch (InterruptedException e) {
                e.printStackTrace();
                return "Error";
            }
        }, executor);
        future.thenAccept(result -> System.out.println("Async result: " + result));
        future.join();
    }
}

6. Using ForkJoinPool

When a large task can be split into many smaller subtasks, ForkJoinPool (Java 7) provides a work‑stealing thread pool designed for divide‑and‑conquer algorithms.

ForkJoinPool’s key features are task splitting (fork), result joining (join), and work‑stealing for efficient parallelism.
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 from 1 to 100: " + result);
    }
    static class SumTask extends RecursiveTask
{
        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();
            }
        }
    }
}

7. Spring @Async

Spring provides the @Async annotation to run a method asynchronously in a separate thread.

Enable async support with @EnableAsync on a configuration class.

Mark the target method with @Async .

Optionally configure a custom Executor bean for better control.

@SpringBootApplication
@EnableAsync
public class AsyncDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
    }
}

Service implementation:

@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();
        }
    }
}

Spring’s default executor is SimpleAsyncTaskExecutor , which creates a new thread for each call. It is recommended to define a custom ThreadPoolTaskExecutor :

@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;
    }
}

Use the custom executor with @Async("taskExecutor") on the method.

8. MQ (Message Queue) Implementation

Message queues are often used for asynchronous processing, decoupling, and traffic shaping. A typical flow is to persist user data, then send a registration‑success message to a queue, which a consumer reads and sends email/SMS.

// User registration method
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);
}
@Service
public class NotificationService {
    @RabbitListener(queues = "registrationQueue")
    public void handleRegistrationNotification(String message) {
        System.out.println("Sending registration notification: " + message);
        sendSms(message);
        sendEmail(message);
    }
}

9. Using Hutool's ThreadUtil

Hutool is a utility library that offers ThreadUtil for simple asynchronous task execution.

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.11</version>
</dependency>

Example:

public class Test {
    public static void main(String[] args) {
        System.out.println("TianLuo main thread");
        ThreadUtil.execAsync(() -> {
            System.out.println("TianLuo async test: " + Thread.currentThread().getName());
        });
    }
}
// Output:
// TianLuo main thread
// TianLuo async test: pool-1-thread-1

These nine techniques cover low‑level thread handling to high‑level framework solutions, allowing developers to choose the most suitable asynchronous strategy for their Java applications.

JavaSpringasynchronousCompletableFutureThreadMQExecutor
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

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.