Java Concurrency
Key APIs, pitfalls, and patterns (Java 17 21)
Crash Notes
© 2025 Short reference for students and professionals.
1. Threads model
1.1 Platform vs Virtual Threads
Platform threads: 1:1 OS threads; heavier context switches.
Virtual threads (Java 21): cheap fibers scheduled by JVM; great for IO bound workloads.
// Java 21
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor())
IntStream.range(0, 1_000).forEach(i -> executor.submit(() -> fetch(i)));
}
1.2 Structured Concurrency
Treat concurrent subtasks as a unit of work; propagate cancel/failure as a group.
// Preview API shape (conceptual)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var u = scope.fork(() -> loadUser());
var o = scope.fork(() -> loadOrders());
scope.join(); scope.throwIfFailed();
return merge(u.get(), o.get());
}
Prefer virtual threads for IO; keep CPU bound tasks on bounded pools.
2. Synchronization & atomics
2.1 synchronized vs Lock
synchronized: simple, reentrant, JVM managed; use for straightforward critical sections.
ReentrantLock: tryLock(), interruptible; fair locks when needed; beware of deadlocks.
final ReentrantLock lock = new ReentrantLock();
void safe() {
if (lock.tryLock()) try { critical(); } finally { lock.unlock(); }
}
2.2 Atomics & Adders
AtomicReference/Long/StampedLock for fine grained control; LongAdder for hot counters.
AtomicReference<State> st = new AtomicReference<>(INIT);
st.updateAndGet(s -> s.advance());
Rule: minimize shared mutable state; prefer immutability and message passing.
3. Executors & CompletableFuture
3.1 Pool sizing
CPU bound: size cores; IO bound: use virtual threads or large pools with back pressure.
3.2 CompletableFuture patterns
thenCompose for chaining async steps; thenCombine for fan in; allOf for fan out.
time outs + orTimeout; handle for fallbacks; minimal blocking joins.
CompletableFuture<User> user = api.loadUser(id);
CompletableFuture<List<Order>> orders = api.loadOrders(id);
var result = user.thenCombine(orders, Service::merge)
.orTimeout(3, TimeUnit.SECONDS)
.exceptionally(Service::fallback);
Use CF for composition; avoid nested futures and blocking get().
4. Collections, pitfalls, debugging
4.1 Concurrent collections
ConcurrentHashMap, CopyOnWriteArrayList; beware write heavy patterns with COW.
Map.computeIfAbsent is atomic; no external double checked locking needed.
cache.computeIfAbsent(key, k -> loader.apply(k));
4.2 Common pitfalls
Blocking calls inside synchronized blocks; unbounded queues; swallowing
InterruptedException.
Deadlocks from inverted lock ordering; visibility bugs without volatile/atomics.
4.3 Debugging
Thread dumps (jcmd/jstack); async profiler; allocation tracking.
Add thread names; MDC correlation IDs; structured logs.
Cheat: set -Djdk.tracePinnedThreads=full to catch pinned virtual threads.
5. Cheatsheet
Quick rules
Prefer immutable DTOs; confine mutability; favor message passing.
Virtual threads for IO; bounded pools for CPU work.
Never block inside synchronized; keep critical sections tiny.
Use timeouts, deadlines, and cancellation everywhere.
Measure: p95 latency per step, queue sizes, and pool saturation.
Practice: rewrite a blocking IO service using virtual threads and deadlines.