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
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.