Eight Ways to Implement Asynchronous Execution in Java
This article introduces eight common Java asynchronous implementation techniques—including raw threads, Future, CompletableFuture, Spring @Async, ApplicationEvent, message queues, third‑party utilities like Hutool ThreadUtil and Guava ListenableFuture—explaining their usage, code examples, advantages, and drawbacks for improving performance in scenarios such as sending SMS or emails.
Asynchronous execution is familiar to developers; many real‑world scenarios benefit from it because it can greatly reduce request‑chain latency, such as sending SMS, emails, or updating data asynchronously.
Eight Ways to Implement Asynchronous Execution
Thread
Future
CompletableFuture
Spring annotation @Async
Spring ApplicationEvent
Message Queue
Third‑party async frameworks (e.g., Hutool ThreadUtil)
Guava async
What Is Asynchronous?
Consider a typical order‑placement flow. In a synchronous process, the system must wait for the SMS‑sending method to finish before it can award points, causing the whole request to block if the point‑granting operation is slow.
Since sending SMS and awarding points are independent, they can be performed concurrently using asynchronous techniques, allowing both operations to run at the same time.
1. Thread Asynchronous
public class AsyncThread extends Thread {
@Override
public void run() {
System.out.println("Current thread name:" + Thread.currentThread().getName() + " Send email success!");
}
public static void main(String[] args) {
AsyncThread asyncThread = new AsyncThread();
asyncThread.run();
}
}Creating a new Thread for each task wastes resources; using a thread pool is more efficient.
private ExecutorService executorService = Executors.newCachedThreadPool();
public void fun() {
executorService.submit(new Runnable() {
@Override
public void run() {
log.info("Execute business logic...");
}
});
}The business logic can be wrapped in a Runnable or Callable and submitted to the pool.
2. Future Asynchronous
@Slf4j
public class FutureManager {
public String execute() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(1);
Future
future = executor.submit(new Callable
() {
@Override
public String call() throws Exception {
System.out.println("--- task start ---");
Thread.sleep(3000);
System.out.println("--- task finish ---");
return "this is future execute final result!!!";
}
});
// Blocking call when result is needed
String result = future.get();
log.info("Future get result: {}", result);
return result;
}
@SneakyThrows
public static void main(String[] args) {
FutureManager manager = new FutureManager();
manager.execute();
}
}Output:
--- task start ---
--- task finish ---
Future get result: this is future execute final result!!!Shortcomings of Future
Cannot passively receive task completion notifications; the main thread must actively call get() .
Futures are isolated; chaining multiple asynchronous tasks requires manual handling, which is why CompletableFuture was introduced.
Lacks robust error‑handling; exceptions must be caught when calling get() .
3. CompletableFuture Asynchronous
public class CompletableFutureCompose {
@SneakyThrows
public static void thenRunAsync() {
CompletableFuture
cf1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread() + " cf1 do something....");
return 1;
});
CompletableFuture
cf2 = cf1.thenRunAsync(() -> {
System.out.println(Thread.currentThread() + " cf2 do something...");
});
// Wait for tasks to complete
System.out.println("cf1 result ->" + cf1.get());
System.out.println("cf2 result ->" + cf2.get());
}
public static void main(String[] args) {
thenRunAsync();
}
}CompletableFuture uses an internal ForkJoinPool for task execution; a custom pool can be supplied if needed.
4. Spring @Async Asynchronous
Custom async thread pool configuration:
@EnableAsync
@Configuration
public class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("System max threads: " + cores);
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(99999);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("asyncServiceExecutor-");
executor.setAwaitTerminationSeconds(60);
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}Async service interface and implementation:
public interface AsyncService {
MessageResult sendSms(String callPrefix, String mobile, String actionType, String content);
MessageResult sendEmail(String email, String subject, String content);
}
@Slf4j
@Service
public class AsyncServiceImpl implements AsyncService {
@Autowired
private IMessageHandler messageHandler;
@Override
@Async("taskExecutor")
public MessageResult sendSms(String callPrefix, String mobile, String actionType, String content) {
try {
Thread.sleep(1000);
messageHandler.sendSms(callPrefix, mobile, actionType, content);
} catch (Exception e) {
log.error("Send SMS exception -> ", e);
}
return null;
}
@Override
@Async("taskExecutor")
public MessageResult sendEmail(String email, String subject, String content) {
try {
Thread.sleep(1000);
messageHandler.sendEmail(email, subject, content);
} catch (Exception e) {
log.error("Send email exception -> ", e);
}
return null;
}
}In practice, using a custom thread pool with @Async is recommended over the default pool.
5. Spring ApplicationEvent Asynchronous
Define an event class:
public class AsyncSendEmailEvent extends ApplicationEvent {
private String email;
private String subject;
private String content;
private String targetUserId;
// getters and setters omitted
}Define an event handler that processes the event asynchronously:
@Slf4j
@Component
public class AsyncSendEmailEventHandler implements ApplicationListener
{
@Autowired
private IMessageHandler messageHandler;
@Async("taskExecutor")
@Override
public void onApplicationEvent(AsyncSendEmailEvent event) {
if (event == null) return;
messageHandler.sendEmail(event.getEmail(), event.getSubject(), event.getContent());
}
}If errors occur, combine with Spring Retry for compensation.
6. Message Queue
Producer sending delayed callback messages:
@Slf4j
@Component
public class CallbackProducer {
@Autowired
AmqpTemplate amqpTemplate;
public void sendCallbackMessage(CallbackDTO callbackDTO, final long delayTimes) {
log.info("Producer sends message, callbackDTO: {}", callbackDTO);
amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(),
CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(),
JsonMapper.getInstance().toJson(callbackDTO),
message -> {
message.getMessageProperties().setHeader("x-delay", delayTimes);
message.getMessageProperties().setCorrelationId(callbackDTO.getSdkId());
return message;
});
}
}Consumer handling the callback:
@Slf4j
@Component
@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory")
public class CallbackConsumer {
@Autowired
private IGlobalUserService globalUserService;
@RabbitHandler
public void handle(String json, Channel channel, @Headers Map
headers) throws Exception {
if (headers.get("error") != null) {
channel.basicNack((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false, true);
return;
}
try {
CallbackDTO callbackDTO = JsonMapper.getInstance().fromJson(json, CallbackDTO.class);
globalUserService.execute(callbackDTO);
channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
} catch (Exception e) {
log.error("Callback failed -> {}", e);
}
}
}7. ThreadUtil Asynchronous Tool
@Slf4j
public class ThreadUtils {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
ThreadUtil.execAsync(() -> {
ThreadLocalRandom random = ThreadLocalRandom.current();
int number = random.nextInt(20) + 1;
System.out.println(number);
});
log.info("Current thread index: " + i);
}
log.info("task finish!");
}
}8. Guava Asynchronous (ListenableFuture)
Guava's ListenableFuture extends the JDK Future and adds addListener(Runnable, Executor) for callback handling, eliminating the need for busy‑waiting.
Creating a ListeningExecutorService and submitting a task:
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
final ListenableFuture
listenableFuture = executorService.submit(new Callable
() {
@Override
public Integer call() throws Exception {
log.info("callable execute...");
TimeUnit.SECONDS.sleep(1);
return 1;
}
});Adding success and failure callbacks:
Futures.addCallback(listenableFuture, new FutureCallback
() {
@Override
public void onSuccess(Integer result) {
System.out.println("Get listenable future's result with callback " + result);
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
});Source: juejin.cn/post/7165147306688249870
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.