Part 3 · 2 chapters · ~12 min

Concurrency and Virtual Threads

The Java memory model and synchronized, java.util.concurrent (executors, locks, atomics, concurrent collections), CompletableFuture, virtual threads measured, structured concurrency and scoped values, reactive programming with Reactor and when it is still worth it, and connection pools as the real limit.

5

From thread pools to virtual threads

code
// measured on this machine (Java 26):
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) ex.submit(() -> { Thread.sleep(1000); return 0; });
}                                                     // 1,490 ms
try (var ex = Executors.newFixedThreadPool(200)) {
    for (int i = 0; i < 2_000; i++) ex.submit(() -> { Thread.sleep(1000); return 0; });
}                                                     // 10,049 ms
VIRTUAL THREADS, MEASURED
Java 26 on this machine: tasks that each block for 1 second
100,000 tasks, virtual threads1,490 ms2,000 tasks, 200 platform threads10,049 ms
swipe the figure sideways, or tap expand for full screen
1/4
platform threads
A classic thread pool with 200 platform (OS) threads runs 200 blocking tasks at a time: 2,000 tasks sleeping one second need ten waves, 10,049 ms measured.
200 at a time: ten wavesthread-per-request caps concurrency
6

java.util.concurrent and structured concurrency

code
// CompletableFuture: compose async steps
CompletableFuture<Quote> quote = CompletableFuture.supplyAsync(() -> rates.quote(pair), executor)
    .orTimeout(500, TimeUnit.MILLISECONDS)
    .exceptionally(e -> Quote.stale(pair));

// structured concurrency (preview in recent JDKs): subtasks cannot outlive the scope
try (var scope = StructuredTaskScope.open()) {
    var acct = scope.fork(() -> accounts.find(id));
    var limits = scope.fork(() -> limitsService.get(id));
    scope.join();                                      // first failure cancels the other
    return new Dashboard(acct.get(), limits.get());
}

// concurrent building blocks
ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent(route, k -> new LongAdder()).increment();