Asynchronous Java Programming: Threads, Futures, CompletableFuture & Spring @Async
This article introduces asynchronous programming concepts in Java, explaining how multithreading, Future, FutureTask, CompletableFuture, and Spring's @Async annotation can transform synchronous workflows into high‑throughput, low‑latency solutions, with code examples and practical guidance for implementing each technique.
Hello, I am Su San. Many friends asked me to summarize asynchronous programming, so today we discuss this topic.
Early systems were synchronous and easy to understand. When a user creates an e‑commerce order, the business logic can be long and increase overall response time. To improve performance, developers separate non‑core tasks from the main flow, creating the prototype of asynchronous programming.
Asynchronous programming is a way to let programs run concurrently. It allows multiple events to happen at the same time; when a method that takes a long time is called, it does not block the current execution flow, and the program can continue running.
The core idea is to use multithreading to turn serial operations into parallel ones. An asynchronous design can significantly reduce thread waiting, greatly improving system performance and reducing latency in high‑throughput scenarios.
1. Thread
Extending the Thread class is the simplest way to create an asynchronous thread.
First, create a Thread subclass (as a regular class or anonymous inner class), instantiate it, and start it with start().
public class AsyncThread extends Thread {
@Override
public void run() {
System.out.println("Current thread name:" + this.getName() + ", executing thread name:" + Thread.currentThread().getName() + "-hello");
}
} public static void main(String[] args) {
// Simulate business process
// ...
// Create asynchronous thread
AsyncThread asyncThread = new AsyncThread();
// Start asynchronous thread
asyncThread.start();
}Creating a new Thread for each task wastes resources. Instead, use a thread pool:
@Bean(name = "executorService")
public ExecutorService downloadExecutorService() {
return new ThreadPoolExecutor(20, 40, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000),
new ThreadFactoryBuilder().setNameFormat("defaultExecutorService-%d").build(),
(r, executor) -> log.error("defaultExecutor pool is full! "));
}Wrap business logic in a Runnable or Callable and submit it to the thread pool.
2. Future
When a task needs to return a result, Java 1.5 introduced Callable and Future. Future provides methods to cancel, check completion, and retrieve results.
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}cancel(): cancels the task, returns true if successful.
isCancelled(): true if the task was cancelled before normal completion.
isDone(): true if the task has finished.
get(): blocks until the task completes and returns the result.
get(long timeout, TimeUnit unit): returns the result within the given time or null.
Example:
public class CallableAndFuture {
public static ExecutorService executorService = new ThreadPoolExecutor(4, 40,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(1024), new ThreadFactoryBuilder()
.setNameFormat("demo-pool-%d").build(), new ThreadPoolExecutor.AbortPolicy());
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Asynchronous processing, Callable returns result";
}
}
public static void main(String[] args) {
Future<String> future = executorService.submit(new MyCallable());
try {
System.out.println(future.get());
} catch (Exception e) {
// handle
} finally {
executorService.shutdown();
}
}
}Callable produces a result; Future retrieves that result.
3. FutureTask
FutureTaskimplements RunnableFuture, which extends both Runnable and Future. Therefore, a FutureTask can be submitted to a ThreadPoolExecutor or executed directly by a Thread, and it also allows obtaining the task result.
Constructors:
public FutureTask(Callable<V> callable)
public FutureTask(Runnable runnable, V result)Commonly used to wrap Callable or Runnable for execution in a thread pool.
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
System.out.println("Child thread starts calculation:");
int sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
return sum;
});
executor.submit(futureTask);
try {
System.out.println("Task result sum: " + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();Callable is for producing a result; Future is for obtaining the result.
4. CompletableFuture
Using Future 's get() method blocks the thread and hurts performance. Since JDK 1.8, CompletableFuture provides a non‑blocking, callback‑based approach based on asynchronous functional programming.
Advantages:
Automatically invokes a callback when the asynchronous task completes.
Automatically invokes a callback when the task fails.
The main thread sets up callbacks and does not need to monitor task execution.
Tea‑making example:
// Task 1: Wash kettle → Boil water
CompletableFuture<Void> f1 =
CompletableFuture.runAsync(() -> {
System.out.println("T1: Wash kettle...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T1: Boil water...");
sleep(15, TimeUnit.SECONDS);
});
// Task 2: Wash teapot → Wash cup → Get tea leaves
CompletableFuture<String> f2 =
CompletableFuture.supplyAsync(() -> {
System.out.println("T2: Wash teapot...");
sleep(1, TimeUnit.SECONDS);
System.out.println("T2: Wash cup...");
sleep(2, TimeUnit.SECONDS);
System.out.println("T2: Get tea leaves...");
sleep(1, TimeUnit.SECONDS);
return "Longjing";
});
// Task 3: After both f1 and f2 complete, make tea
CompletableFuture<String> f3 =
f1.thenCombine(f2, (__ , tf) -> {
System.out.println("T1: Got tea leaves: " + tf);
System.out.println("T1: Make tea...");
return "Serve tea: " + tf;
});
System.out.println(f3.join()); CompletableFutureoffers about 50 methods for serial, parallel, combinational, and error‑handling scenarios.
5. SpringBoot @Async
Beyond hard‑coded approaches, Spring Boot provides an annotation‑based solution. Mark a method with @Async and it will be executed asynchronously.
Enable async support:
@SpringBootApplication
@EnableAsync
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}Define a custom thread pool:
@Configuration
public class ThreadPoolConfiguration {
@Bean(name = "defaultThreadPoolExecutor", destroyMethod = "shutdown")
public ThreadPoolExecutor systemCheckPoolExecutorService() {
return new ThreadPoolExecutor(3, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(10000),
new ThreadFactoryBuilder().setNameFormat("default-executor-%d").build(),
(r, executor) -> log.error("system pool is full! "));
}
}Apply the annotation to a method:
@Service
public class AsyncServiceImpl implements AsyncService {
@Async("defaultThreadPoolExecutor")
public Boolean execute(Integer num) {
System.out.println("Thread: " + Thread.currentThread().getName() + " , task: " + num);
return true;
}
}Steps to use @Async:
Add @EnableAsync to the application or configuration class.
Annotate the target method with @Async.
Ensure the class containing the async method is managed by the Spring container.
6. Spring ApplicationEvent
Spring’s event mechanism decouples components using the observer pattern. Publish an ApplicationEvent and listeners implementing ApplicationListener will be notified.
public class OrderEvent extends AbstractGenericEvent<OrderModel> {
public OrderEvent(OrderModel source) {
super(source);
}
} @Component
public class OrderEventListener implements ApplicationListener<OrderEvent> {
@Override
public void onApplicationEvent(OrderEvent event) {
System.out.println("[OrderEventListener] Handling event! " + JSON.toJSONString(event.getSource()));
}
} OrderModel orderModel = new OrderModel();
orderModel.setOrderId((long) i);
orderModel.setBuyerName("Tom-" + i);
orderModel.setSellerName("judy-" + i);
orderModel.setAmount(100L);
SpringUtils.getApplicationContext().publishEvent(new OrderEvent(orderModel));By default, Spring’s event dispatching is synchronous. To make it asynchronous, configure a SimpleApplicationEventMulticaster with a TaskExecutor:
@Component
public class SpringConfiguration {
@Bean
public SimpleApplicationEventMulticaster applicationEventMulticaster(@Qualifier("defaultThreadPoolExecutor") ThreadPoolExecutor defaultThreadPoolExecutor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(defaultThreadPoolExecutor);
return multicaster;
}
}7. Message Queue
Asynchronous architecture is common in internet systems, and message queues are a natural fit, offering ultra‑high throughput and ultra‑low latency.
Key roles: producer (encapsulates requests into messages), the queue (buffers messages), and consumer (pulls and processes messages). Queues support point‑to‑point and publish‑subscribe modes. Popular implementations include RabbitMQ, Kafka, RocketMQ, ActiveMQ, and Pulsar.
Using a message queue as middleware efficiently implements asynchronous programming.
Code repository: https://github.com/aalansehaiyang/wx-project
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
