A Brief Guide to Several Ways of Implementing Asynchronous Programming in Java
In our daily development work, we often talk about asynchronous programming.
For example, in a user registration API, after a user successfully registers, we might send them a notification email asynchronously.
But here’s a question for you:
Do you know how many different ways there are to implement asynchronous programming in Java?
Let me walk you through the following commonly used approaches:
• Using Thread
and Runnable
• Using thread pools provided by Executors
• Using a custom thread pool
• Using Future
and Callable
• Using CompletableFuture
• Using ForkJoinPool
• Using Spring's @Async
for asynchronous methods
• Using Message Queues (MQ) for async processing
1. Using Thread and Runnable
Thread and Runnable are the most basic ways to implement asynchronous programming in Java.
You can directly use them to create and manage threads.
public class Test {
public static void main(String[] args) {
System.out.println("Main thread (registration) : " + Thread.currentThread().getName());
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("Asynchronous thread test (email notification): " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
However, in real-world development, it's generally not recommended to use Thread and Runnable directly, due to the following drawbacks:
High resource consumption: Creating a new thread each time consumes system resources. Frequent creation and destruction of threads can lead to performance degradation.
Difficult to manage: Manually handling thread life cycles, exception handling, and task scheduling is complex and error-prone.
Lacks scalability: It's hard to control the number of concurrent threads, which can easily exhaust system resources.
No thread reuse: A new thread is created for each task, which prevents reuse and results in low efficiency.
2. Using Thread Pools Provided by Executors
To address the drawbacks of using Thread and Runnable directly, we can turn to thread pools. Thread pools offer several advantages:
Thread management: Thread pools manage threads for you, avoiding the overhead of frequently creating and destroying threads. After all, a thread is an object — creating it involves class loading, and destroying it involves garbage collection, both of which consume resources.
Improved response time: When a task arrives, retrieving a thread from the pool is much faster than creating a brand-new one.
Thread reuse: Once a thread finishes a task, it's returned to the pool for reuse, which helps conserve system resources.
Here’s a simple demo:
public class Test {
public static void main(String[] args) {
System.out.println("Main thread (Registration): " + Thread.currentThread().getName());
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(() -> {
System.out.println("Asynchronous thread via thread pool (Email Notification): " + Thread.currentThread().getName());
});
}
}
Output:
Main thread (Registration): main
Asynchronous thread via thread pool (Email Notification): pool-1-thread-1
3. Using a Custom Thread Pool
Although using built-in thread pools from Executors (like newFixedThreadPool) is simple and convenient, it has a major downside — the task queue it uses is unbounded!
Specifically, newFixedThreadPool uses a LinkedBlockingQueue by default, which is an unbounded queue (with a default capacity of Integer.MAX_VALUE).
If tasks are submitted faster than they can be processed, the queue will keep growing, potentially leading to out-of-memory (OOM) errors.
That’s why it’s generally recommended to use a custom thread pool for better control over asynchronous execution.
public class Test {
public static void main(String[] args) {
// Custom thread pool
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // Core thread count
4, // Maximum thread count
60, // Keep-alive time for idle threads
TimeUnit.SECONDS, // Time unit
new ArrayBlockingQueue<>(8), // Task queue
Executors.defaultThreadFactory(), // Thread factory
new ThreadPoolExecutor.CallerRunsPolicy() // Rejection policy
);
System.out.println("Main thread (Registration): " + Thread.currentThread().getName());
executor.execute(() -> {
try {
Thread.sleep(500); // Simulate a time-consuming task
System.out.println("Asynchronous task with custom thread pool (Email Notification): " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
Output:
Main thread (Registration): main
Asynchronous task with custom thread pool (Email Notification): pool-1-thread-1
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Custom thread pool
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // Core thread count
4, // Maximum thread count
60, // Idle thread keep-alive time
TimeUnit.SECONDS, // Time unit
new ArrayBlockingQueue<>(8), // Task queue
Executors.defaultThreadFactory(), // Thread factory
new ThreadPoolExecutor.CallerRunsPolicy() // Rejection policy
);
System.out.println("Main thread (Registration): " + Thread.currentThread().getName());
Callable<String> task = () -> {
Thread.sleep(1000); // Simulate a time-consuming task
System.out.println("Asynchronous task with custom thread pool (Email Notification): " + Thread.currentThread().getName());
return "Hello: I am done for job!";
};
Future<String> future = executor.submit(task);
String result = future.get(); // Blocks until the task is complete
System.out.println("Asynchronous result: " + result);
}
}
Main thread (Registration): main Asynchronous task with custom thread pool (Email Notification): pool-1-thread-1 Asynchronous result: Hello: I am done for job!
5. Using
CompletableFuture
CompletableFuture
was introduced in Java 8 and provides much more powerful capabilities for asynchronous programming.It supports features such as chained calls, exception handling, and combining multiple asynchronous tasks.
Here’s a demo:
public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { // Custom thread pool ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // Core thread count 4, // Maximum thread count 60, // Idle thread keep-alive time TimeUnit.SECONDS, // Time unit new ArrayBlockingQueue<>(8), // Task queue Executors.defaultThreadFactory(), // Thread factory new ThreadPoolExecutor.CallerRunsPolicy() // Rejection policy ); System.out.println("Main thread (main): " + Thread.currentThread().getName()); CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); // Simulate a time-consuming task System.out.println("CompletableFuture async task (async-task): " + Thread.currentThread().getName()); return "Hello: I am async task!"; } catch (InterruptedException e) { e.printStackTrace(); } return "Hello: I am async task!"; }, executor); future.thenAccept(result -> System.out.println("Asynchronous result: " + result)); future.join(); // Wait for completion } }
Comments
Post a Comment