← All cheat sheets ·

Concurrency

Thread lifecycle, JMM primitives, locks, executors, CompletableFuture, virtual threads and a deadlock checklist.

Thread lifecycle

State (Thread.State) Entered by Left by
NEW new Thread(r) start()
RUNNABLE start(), lock acquired, wait/sleep ended (includes OS-level running and blocked on I/O) scheduler, blocking calls
BLOCKED waiting to enter a synchronized block/monitor monitor acquired
WAITING Object.wait(), Thread.join(), LockSupport.park() (no timeout) notify, join target ends, unpark
TIMED_WAITING sleep(ms), wait(ms), join(ms), parkNanos timeout or signal
TERMINATED run() returns or throws -
  • start() once only (second call throws IllegalThreadStateException); calling run() runs on the caller thread.
  • ReentrantLock.lock() parks the thread, so it shows as WAITING, not BLOCKED.
  • Interrupt is cooperative: interrupt() sets a flag; blocking methods throw InterruptedException and clear the flag. Restore it: Thread.currentThread().interrupt().
  • wait/notify must be called while holding the monitor and wait inside a while loop (spurious wakeups).
  • Thread.stop/suspend/resume are deprecated; stop throws UnsupportedOperationException from Java 20.

Java Memory Model essentials

Tool Atomicity Visibility Ordering (happens-before)
plain field no (long/double tearing on 32-bit allowed) no no
volatile single read/write only (i++ is not atomic) yes write happens-before later read
synchronized whole block yes (release on exit, acquire on enter) yes
Atomic* CAS ops (incrementAndGet, compareAndSet) yes yes
final fields - safe publication after constructor if this does not escape yes
  • Other happens-before edges: Thread.start(), Thread.join(), executor submit -> task, task -> Future.get(), ConcurrentHashMap put -> later get.
  • Double-checked locking requires volatile on the instance field.
  • LongAdder beats AtomicLong under high write contention; sum() is not an atomic snapshot.
  • ABA problem: AtomicStampedReference / AtomicMarkableReference.
class Lazy {
    private static volatile Lazy inst;
    static Lazy get() {
        Lazy l = inst;
        if (l == null) synchronized (Lazy.class) {
            if ((l = inst) == null) inst = l = new Lazy();
        }
        return l;
    }
}   // simpler: holder-class idiom or enum singleton

synchronized vs locks

Feature synchronized ReentrantLock ReadWriteLock StampedLock
Release automatic manual finally unlock() manual manual
tryLock / timeout no yes yes yes
Interruptible acquire no lockInterruptibly() yes yes
Fairness no optional new ReentrantLock(true) optional no
Conditions one (wait/notify) many (newCondition()) read lock has none no
Reentrant yes yes yes no
Virtual thread pinning pins carrier in Java 21 (fixed in 24) does not pin does not pin does not pin
lock.lock();
try { /* critical section */ } finally { lock.unlock(); }
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { try { ... } finally { lock.unlock(); } }
  • StampedLock.tryOptimisticRead() + validate(stamp) for read-heavy code; fall back to a read lock on failure.
  • Upgrading read lock to write lock in ReentrantReadWriteLock deadlocks; downgrading (write -> read) is allowed.
  • Lock coordination utilities: CountDownLatch (one-shot), CyclicBarrier (reusable), Semaphore (permits), Phaser, Exchanger.

ExecutorService

Factory Pool Queue Risk
newFixedThreadPool(n) n core = max unbounded LinkedBlockingQueue queue grows without bound -> OOM
newCachedThreadPool() 0..Integer.MAX_VALUE, 60s idle SynchronousQueue unbounded thread creation
newSingleThreadExecutor() 1 unbounded serial, unbounded queue
newScheduledThreadPool(n) n delay queue task throwing cancels future runs
newWorkStealingPool() ForkJoinPool, daemon threads per-worker deques not for blocking tasks
newVirtualThreadPerTaskExecutor() (21) thread per task none do not pool virtual threads
  • ThreadPoolExecutor order: fewer than core threads -> new thread; else enqueue; queue full -> thread up to max; else rejection policy (AbortPolicy default, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy).
  • Production: build ThreadPoolExecutor directly with a bounded ArrayBlockingQueue, named threads, explicit rejection policy.
  • CPU-bound pool size ~ cores (+1); IO-bound ~ cores * (1 + wait/compute).
  • submit() swallows exceptions into the Future; execute() sends them to the uncaught handler.
  • Shutdown: shutdown() (finish queued), shutdownNow() (interrupt), then awaitTermination. Java 19+: ExecutorService is AutoCloseable (try-with-resources calls close()).
var pool = new ThreadPoolExecutor(4, 8, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    Thread.ofPlatform().name("worker-", 0).factory(),     // Java 21 builder
    new ThreadPoolExecutor.CallerRunsPolicy());

CompletableFuture

Method Meaning
supplyAsync(sup, executor) / runAsync start async task (default pool: ForkJoinPool.commonPool())
thenApply(fn) / thenAccept / thenRun transform / consume / side-effect (same thread or completer)
thenCompose(fn) flatMap: fn returns a CompletableFuture
thenCombine(other, bi) join two independent futures
allOf(...) / anyOf(...) wait for all / first
exceptionally(fn) recover from exception
handle((v, ex) -> ...) always runs; success or failure
whenComplete((v, ex) -> ...) observe, does not change result
orTimeout(t, unit) / completeOnTimeout(v, t, unit) Java 9+ timeouts
join() vs get() join throws unchecked CompletionException; get throws checked ExecutionException
  • Always pass your own executor for blocking work; the common pool has cores - 1 threads.
  • *Async variants hop to an executor; non-async callbacks run on the completing thread (or the caller if already done).
  • Exceptions propagate downstream wrapped in CompletionException.
var user  = CompletableFuture.supplyAsync(() -> userSvc.get(id), io);
var order = CompletableFuture.supplyAsync(() -> orderSvc.get(id), io);
var view  = user.thenCombine(order, View::new)
                .orTimeout(2, TimeUnit.SECONDS)
                .exceptionally(ex -> View.empty());

Virtual threads (Java 21, JEP 444)

  • Create: Thread.startVirtualThread(r), Thread.ofVirtual().name("v-", 0).start(r), Executors.newVirtualThreadPerTaskExecutor().
  • Spring Boot 3.2+: spring.threads.virtual.enabled=true (Tomcat, @Async, schedulers use virtual threads; requires Java 21).
  • Cheap (~KBs, heap-allocated stacks); one thread per task; never pool them; use Semaphore to limit concurrency.
  • Blocking I/O and LockSupport.park unmount the virtual thread from its carrier; helps IO-bound code, not CPU-bound.
  • Pinning (Java 21): blocking inside synchronized or a native frame holds the carrier. Prefer ReentrantLock. Diagnose with -Djdk.tracePinnedThreads=full. Java 24 (JEP 491) removes synchronized pinning.
  • Beware ThreadLocal heavy use (per-virtual-thread copies) and connection pools sized below concurrency (DB becomes the limit).
  • Preview in 21: StructuredTaskScope (JEP 453) and ScopedValue (JEP 446); need --enable-preview.
  • Thread dumps: jcmd <pid> Thread.dump_to_file -format=json file.json (plain jstack omits virtual threads).

Concurrent collections

Need Use
Shared map ConcurrentHashMap
Sorted concurrent map/set ConcurrentSkipListMap / Set
Read-mostly list CopyOnWriteArrayList
Unbounded non-blocking queue ConcurrentLinkedQueue / ConcurrentLinkedDeque
Bounded hand-off with back-pressure ArrayBlockingQueue
Optionally bounded, separate put/take locks LinkedBlockingQueue
Priority hand-off PriorityBlockingQueue (unbounded)
Direct hand-off SynchronousQueue
Delayed tasks DelayQueue
Per-thread counters LongAdder, LongAccumulator
  • BlockingQueue ops: put/take block, offer/poll(timeout) time out, add/remove throw.
  • Stop consumers with a poison pill or interrupt.

Deadlock checklist

  • Four conditions (all required): mutual exclusion, hold-and-wait, no preemption, circular wait. Break any one.
  • Acquire multiple locks in one global order (e.g. by account id).
  • Use tryLock(timeout) and back off on failure.
  • Never call alien/overridable methods or callbacks while holding a lock.
  • Keep critical sections short; no I/O inside locks.
  • Avoid nested synchronized on different objects; prefer one lock or immutable data.
  • Do not block a pool thread waiting on a task queued to the same pool (thread starvation deadlock).
  • DB: update rows in consistent order; keep transactions short.
  • Detect: jstack -l <pid> prints "Found one Java-level deadlock"; ThreadMXBean.findDeadlockedThreads() programmatically.
  • Livelock = threads keep reacting but make no progress; starvation = low-priority thread never scheduled (use fair locks).

Common pitfalls

  • SimpleDateFormat and Random shared across threads; use DateTimeFormatter, ThreadLocalRandom.
  • parallelStream() uses the common pool; one blocking task slows all parallel streams.
  • ThreadLocal leaks in pools: always remove().
  • synchronized on a Integer/String literal or a reused boxed value shares the lock unintentionally.
  • Non-volatile stop flag may never be seen; use volatile boolean or interrupts.
  • Check-then-act on ConcurrentHashMap (if (!containsKey) put) is racy; use putIfAbsent/computeIfAbsent.