Async programming in Java

Java supports async programming but does not have built-in async/await syntax (unlike Rust or C#).
Async-style work is done with threads, thread pools, and non-blocking result objects (Future, CompletableFuture).
From Java 21 onward, virtual threads make blocking I/O cheap — you write synchronous-looking code while the JVM schedules thousands of tasks on few OS threads.
See also: Thread creation, Java synchronization.

1. Thread + Runnable — basic background task

Oldest approach: spawn a OS thread and return immediately from main. The caller does not get a result object — use join() to wait.

public class ThreadAsync {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("background work on " + Thread.currentThread());
        });
        t.start();   // async — main continues
        t.join();    // wait for completion
        System.out.println("main done");
    }
}
      

2. ExecutorService — thread pool

Submit many tasks to a pool instead of creating a new OS thread per task. Returns a Future for each submission.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorExample {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(4);

        Future<String> future = pool.submit(() -> {
            Thread.sleep(500);
            return "data from DB";
        });

        System.out.println("main continues while task runs...");
        String result = future.get();   // block until result ready
        System.out.println(result);

        pool.shutdown();
    }
}
      

3. Future — result of async computation

Future<T> represents a result that may not be ready yet. Call get() to block and retrieve it (similar to C++ std::future).

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newSingleThreadExecutor();

        Future<Integer> future = pool.submit(() -> {
            Thread.sleep(300);
            return 42;
        });

        System.out.println("waiting...");
        System.out.println("answer = " + future.get());

        pool.shutdown();
    }
}
      

4. CompletableFuture — chain async steps

CompletableFuture (Java 8+) supports callbacks, composition, and combining multiple async operations without blocking main at every step — closest to modern async/await style.

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    static CompletableFuture<String> fetchUser() {
        return CompletableFuture.supplyAsync(() -> {
            sleep(200);
            return "Alice";
        });
    }

    static CompletableFuture<Integer> fetchOrders(String user) {
        return CompletableFuture.supplyAsync(() -> {
            sleep(200);
            return user.equals("Alice") ? 3 : 0;
        });
    }

    static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        fetchUser()
            .thenCompose(CompletableFutureExample::fetchOrders)
            .thenAccept(count -> System.out.println("orders = " + count))
            .join();   // wait for pipeline to finish

        // combine two independent async calls
        CompletableFuture<String> a = fetchUser();
        CompletableFuture<Integer> b = fetchOrders("Alice");
        String summary = a.thenCombine(b, (user, orders) ->
            user + " has " + orders + " orders"
        ).join();
        System.out.println(summary);
    }
}
      

5. Virtual threads — async I/O without callbacks

Java 21+ virtual threads are green threads managed by the JVM. Blocking calls (socket read, JDBC query) park the virtual thread instead of blocking an OS thread — write straight-line code, get async scalability.

import java.util.concurrent.Executors;

public class VirtualThreadExample {
    public static void main(String[] args) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 5; i++) {
                int id = i;
                executor.submit(() -> {
                    System.out.println("task " + id + " on " + Thread.currentThread());
                    // blocking I/O here is fine — JVM parks this virtual thread
                });
            }
        }
        System.out.println("all tasks done");
    }
}
      

Summary: Use CompletableFuture for callback-style async pipelines; use newVirtualThreadPerTaskExecutor() (Java 21+) for high-concurrency blocking I/O with simple synchronous code.