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.
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.newCachedThreadPoolSimple 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-13. 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-14. 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-1These 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.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
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.