Backend Development 14 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
Eight Ways to Implement Asynchronous Execution in Java

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

JavaSpringasynchronousCompletableFutureThreadGuavaMessageQueueFuture
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.