8 Powerful Ways to Implement Asynchronous Programming in Java

This article explains why asynchronous execution shortens request latency and presents eight practical Java techniques—including raw threads, Future, CompletableFuture, Spring @Async, ApplicationEvent, message queues, ThreadUtil, and Guava ListenableFuture—complete with code examples and usage tips.

Architect's Guide
Architect's Guide
Architect's Guide
8 Powerful Ways to Implement Asynchronous Programming in Java

Asynchronous execution is essential for developers to shorten request latency, allowing independent tasks such as sending SMS, emails, or updating data to run concurrently.

Thread

Future

CompletableFuture

Spring annotation @Async

Spring ApplicationEvent

Message queue

Third‑party async frameworks (e.g., Hutool ThreadUtil)

Guava ListenableFuture

What Is Asynchrony?

In a synchronous flow, the method that sends an SMS must wait for the subsequent "grant points" operation to finish, even though the two actions are unrelated. By using asynchronous calls, both operations can proceed in parallel.

The following sections demonstrate each implementation.

4.1 Thread Asynchrony

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 every task wastes resources; a thread pool is preferred.

private ExecutorService executorService = Executors.newCachedThreadPool();
public void fun() {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            log.info("Executing business logic...");
        }
    });
}

4.2 Future Asynchrony

@Slf4j
public class FutureManager {
    public String execute() throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        Future<String> future = executor.submit(new Callable<String>() {
            @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!!!";
            }
        });
        String result = future.get(); // blocks if result not ready
        log.info("Future get result: {}", result);
        return result;
    }
    @SneakyThrows
    public static void main(String[] args) {
        new FutureManager().execute();
    }
}

Output:

--- task start ---
--- task finish ---
Future get result: this is future execute final result!!!

Future Limitations

Cannot receive task completion notifications passively.

Futures are isolated; they cannot form pipelines.

Lacks robust error‑handling; exceptions must be caught from get().

4.3 CompletableFuture Asynchrony

public class CompletableFutureCompose {
    @SneakyThrows
    public static void thenRunAsync() {
        CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread() + " cf1 do something....");
            return 1;
        });
        CompletableFuture<Void> cf2 = cf1.thenRunAsync(() -> {
            System.out.println(Thread.currentThread() + " cf2 do something...");
        });
        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 and can be configured with a custom executor.

4.4 Spring @Async

@Configuration
public class TaskPoolConfig {
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        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;
    }
}
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;

    @Async("taskExecutor")
    public void sendSms(String callPrefix, String mobile, String actionType, String content) {
        try {
            Thread.sleep(1000);
            messageHandler.sendSms(callPrefix, mobile, actionType, content);
        } catch (Exception e) {
            log.error("SMS send exception -> ", e);
        }
    }

    @Async("taskExecutor")
    public void sendEmail(String email, String subject, String content) {
        try {
            Thread.sleep(1000);
            messageHandler.sendEmail(email, subject, content);
        } catch (Exception e) {
            log.error("Email send exception -> ", e);
        }
    }
}

Using a custom thread pool with @Async is recommended over the default executor.

4.5 Spring ApplicationEvent Asynchrony

public class AsyncSendEmailEvent extends ApplicationEvent {
    private String email;
    private String subject;
    private String content;
    private String targetUserId;
}
@Component
public class AsyncSendEmailEventHandler implements ApplicationListener<AsyncSendEmailEvent> {
    @Autowired
    private IMessageHandler messageHandler;

    @Async("taskExecutor")
    @Override
    public void onApplicationEvent(AsyncSendEmailEvent event) {
        if (event == null) return;
        messageHandler.sendEmail(event.getEmail(), event.getSubject(), event.getContent());
    }
}

Combine with Spring Retry for fault tolerance.

4.6 Message Queue Asynchrony

@Component
public class CallbackProducer {
    @Autowired
    AmqpTemplate amqpTemplate;
    public void sendCallbackMessage(CallbackDTO dto, long delay) {
        amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(),
            CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(),
            JsonMapper.getInstance().toJson(dto),
            message -> {
                message.getMessageProperties().setHeader("x-delay", delay);
                message.getMessageProperties().setCorrelationId(dto.getSdkId());
                return message;
            });
    }
}
@Component
@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory")
public class CallbackConsumer {
    @Autowired
    private IGlobalUserService globalUserService;
    @RabbitHandler
    public void handle(String json, Channel channel, @Headers Map<String, Object> headers) throws Exception {
        if (headers.get("error") != null) {
            channel.basicNack((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false, true);
            return;
        }
        CallbackDTO dto = JsonMapper.getInstance().fromJson(json, CallbackDTO.class);
        globalUserService.execute(dto);
        channel.basicAck((Long) headers.get(AmqpHeaders.DELIVERY_TAG), false);
    }
}

4.7 Hutool ThreadUtil

public class ThreadUtils {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            ThreadUtil.execAsync(() -> {
                int number = ThreadLocalRandom.current().nextInt(20) + 1;
                System.out.println(number);
            });
            log.info("Current thread index: " + i);
        }
        log.info("task finish!");
    }
}

4.8 Guava ListenableFuture

ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFuture<Integer> listenableFuture = executorService.submit(() -> {
    log.info("callable execute...");
    TimeUnit.SECONDS.sleep(1);
    return 1;
});
Futures.addCallback(listenableFuture, new FutureCallback<Integer>() {
    @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();
    }
});

The article concludes that these eight methods cover most common scenarios for implementing asynchronous processing in Java.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

springCompletableFutureThreadMessage Queueasynchronous programmingFuture
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

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.