Choosing the right concurrency tool
A short decision path from the task you have to the executor, lock or queue that fits it.
Open this lesson in the learning hubKey points
- CPU-bound work: a fixed pool sized near the core count, or a parallel stream. More threads than cores only adds switching.
- Blocking I/O on Java 21: one virtual thread per task. There is no pool size to tune, and plain blocking code stays readable.
- Several results to combine:
CompletableFuture, orStructuredTaskScopewhen the subtasks must fail together. - Shared state: an atomic for one field, a concurrent collection for a map, a lock only when two fields must change together.
- Handing work between stages: a bounded
BlockingQueue. The bound is your back-pressure and your overload alarm.
Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
static final int TASKS = 200;
public static void main(String[] args) throws Exception {
System.out.println(TASKS + " tasks that each wait 10 ms (pure I/O shape)");
System.out.println("fixed pool of 4 : " + run(Executors.newFixedThreadPool(4)) + " ms");
System.out.println("fixed pool of 64 : " + run(Executors.newFixedThreadPool(64)) + " ms");
System.out.println("virtual per task : " + run(Executors.newVirtualThreadPerTaskExecutor()) + " ms");
System.out.println();
System.out.println("waiting work scales with the thread count, so use virtual threads");
System.out.println("CPU work does not: there a fixed pool of " + Runtime.getRuntime().availableProcessors() + " is the ceiling");
}
static long run(ExecutorService pool) {
AtomicInteger done = new AtomicInteger();
long start = System.nanoTime();
try (pool) { // close() waits for every task
for (int i = 0; i < TASKS; i++) {
pool.submit(() -> { Thread.sleep(10); return done.incrementAndGet(); });
}
}
return (System.nanoTime() - start) / 1_000_000;
}
}
Pick the smallest tool that fits: atomic, then concurrent collection, then lock.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.