Backend Development 12 min read

Eight Ways to Implement Asynchronous Programming in Java

This article introduces the concept of asynchronous execution and presents eight practical Java implementations—including Thread, Future, CompletableFuture, Spring @Async, ApplicationEvent, message queues, ThreadUtil, and Guava ListenableFuture—along with code examples and usage tips for each method.

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

Asynchronous execution is a common technique for reducing latency in scenarios such as sending SMS, emails, or updating data, where the tasks have no direct dependency on each other.

The article lists eight ways to achieve async behavior in Java:

Thread

Future

CompletableFuture

Spring @Async annotation

Spring ApplicationEvent

Message queues (RabbitMQ)

ThreadUtil (Hutool)

Guava ListenableFuture

1. Thread example

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 can waste resources, so a thread pool is recommended.

Thread pool with ExecutorService

private ExecutorService executorService = Executors.newCachedThreadPool();
public void fun() {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            log.info("执行业务逻辑...");
        }
    });
}

2. Future

@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!!!";
            }
        });
        String result = future.get();
        log.info("Future get result: {}", result);
        return result;
    }
    @SneakyThrows
    public static void main(String[] args) {
        new FutureManager().execute();
    }
}

The article notes three drawbacks of Future: no passive result notification, isolation between futures, and limited error handling.

3. CompletableFuture

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...");
        });
        System.out.println("cf1结果->" + cf1.get());
        System.out.println("cf2结果->" + cf2.get());
    }
    public static void main(String[] args) {
        thenRunAsync();
    }
}

CompletableFuture uses the ForkJoinPool internally and can be customized with a user‑defined executor.

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;
    }
}

Methods annotated with @Async("taskExecutor") run in the custom pool, e.g., sending SMS or email.

5. Spring ApplicationEvent

public class AsyncSendEmailEvent extends ApplicationEvent {
    private String email;
    private String subject;
    private String content;
    private String targetUserId;
}
@Component
public class AsyncSendEmailEventHandler implements ApplicationListener
{
    @Autowired
    private IMessageHandler messageHandler;
    @Async("taskExecutor")
    @Override
    public void onApplicationEvent(AsyncSendEmailEvent event) {
        if (event == null) return;
        messageHandler.sendsendEmailSms(event.getEmail(), event.getSubject(), event.getContent(), event.getTargetUserId());
    }
}

Combining ApplicationEvent with Spring Retry can provide compensation for failures.

6. Message Queue (RabbitMQ)

@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
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);
    }
}

These components illustrate asynchronous processing via messaging.

7. ThreadUtil (Hutool)

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("当前第:" + i + "个线程");
        }
        log.info("task finish!");
    }
}

ThreadUtil provides a concise API for fire‑and‑forget tasks.

8. Guava ListenableFuture

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

ListenableFuture extends the JDK Future with a listener mechanism, simplifying callback handling.

In summary, the article demonstrates eight different Java techniques for asynchronous programming, each with code samples and practical considerations, helping developers choose the most suitable approach for their projects.

JavaSpringasynchronousCompletableFutureThreadMessageQueueFuture
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.