Concurrency
Threads, synchronized, locks, ExecutorService, CompletableFuture, virtual threads
Java concurrency is both powerful and notoriously difficult to get right. From low-level synchronized blocks to high-level CompletableFuture pipelines and Java 21's virtual threads, the platform offers a layered concurrency model. Understanding the memory model, happens-before relationships, and when to use which abstraction separates senior Java engineers from juniors.
Key Points
- Thread lifecycle: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
- synchronized method/block: acquires the object's intrinsic lock — only one thread at a time, but no timeout or fairness
- volatile: ensures visibility (prevents CPU caching) but NOT atomicity — volatile int i; i++ is still not thread-safe
- ReentrantLock: explicit lock with tryLock(timeout), lockInterruptibly(), and optional fairness — always unlock in finally
- AtomicInteger / AtomicReference: lock-free CAS (compare-and-swap) operations — faster than synchronized for single variables
- ExecutorService: thread pool abstraction — Executors.newFixedThreadPool(n), newCachedThreadPool(), newScheduledThreadPool(n)
- CompletableFuture (Java 8): async pipeline with thenApply, thenCompose, thenCombine, exceptionally, allOf, anyOf
- Virtual Threads (Java 21): lightweight threads managed by the JVM, not the OS — create millions without exhausting memory; perfect for I/O-bound work
- Java Memory Model (JMM): defines happens-before — a write to a volatile variable HB a subsequent read; monitor unlock HB subsequent lock
| Mechanism | Use case | Timeout? | Fairness? | Overhead |
|---|---|---|---|---|
| synchronized | Simple mutual exclusion | No | No | Low |
| ReentrantLock | Timed/interruptible lock | Yes | Optional | Low |
| ReadWriteLock | Many readers, few writers | Yes | Optional | Low |
| AtomicXxx | Single variable, lock-free | N/A | N/A | Very low |
| ExecutorService | Thread pool task submission | N/A | N/A | Medium |
| CompletableFuture | Async pipeline composition | N/A | N/A | Low |
| Virtual Thread (J21) | Millions of I/O tasks | N/A | N/A | Very low |
Java concurrency: ReentrantLock, CompletableFuture pipeline, Java 21 Structured Concurrency and Virtual Threads
// ReentrantLock — always unlock in finally
private final ReentrantLock lock = new ReentrantLock();
public void transfer(Account to, int amount) {
lock.lock();
try { balance -= amount; to.balance += amount; }
finally { lock.unlock(); }
}
// CompletableFuture pipeline
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> fetchUser(id)) // runs in ForkJoinPool
.thenApply(user -> enrichWithOrders(user)) // transform
.thenCompose(u -> CompletableFuture.supplyAsync(() -> notifyEmail(u)))
.exceptionally(ex -> "default"); // fallback
// Combine two futures
CompletableFuture<UserData> data = CompletableFuture.allOf(futureA, futureB)
.thenApply(v -> merge(futureA.join(), futureB.join()));
// Java 21 Virtual Threads — structured concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<User> user = scope.fork(() -> fetchUser(id));
Future<Orders> orders = scope.fork(() -> fetchOrders(id));
scope.join().throwIfFailed();
return new Page(user.resultNow(), orders.resultNow());
}
// Virtual thread executor — drop-in for thread-per-request
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i ->
exec.submit(() -> handleRequest(i))
);
}Real-World Example
Before virtual threads, a typical web server with a 200-thread pool could handle 200 concurrent requests. With virtual threads, the same server handles 100,000+ because I/O blocking unmounts the virtual thread from the carrier OS thread, freeing it for another task. Spring Boot 3.2+ and Tomcat 10 automatically use virtual threads when running on Java 21 — zero code changes needed.