TL;DR
- Concurrency is about coordinating overlapping tasks; parallelism is about executing work simultaneously. They solve different problems.
- Correct concurrent code must control atomicity, visibility, and ordering. A thread-safe collection does not automatically make a multi-step workflow thread-safe.
- Prefer immutable data, ownership boundaries, queues, and high-level
java.util.concurrentutilities before writing custom locking. - Use bounded platform-thread pools for CPU-bound work. Use virtual threads for large numbers of mostly-blocking tasks, but limit scarce downstream resources with semaphores.
- Treat interruption as cooperative cancellation. Preserve the interrupt flag when a method cannot propagate
InterruptedException. - Diagnose concurrency through evidence: thread dumps, Java Flight Recorder, contention metrics, queue depth, rejected tasks, and stress tests.
Context
Java concurrency spans several layers that are easy to mix together:
- Scheduling — which task runs on which thread?
- Memory semantics — when does one thread observe another thread's writes?
- Mutual exclusion — which operations must not overlap?
- Coordination — how do tasks wait, signal, hand off, cancel, and fail together?
- Capacity control — how much concurrent work can the application and its dependencies sustain?
Most production failures happen because code solves one layer while ignoring another. A synchronized method may protect an invariant but still call a slow remote dependency while holding the lock. A virtual-thread executor may scale request handling but overwhelm a database limited to 40 connections. A ConcurrentHashMap may make individual operations safe while a check-then-act sequence remains racy.
This guide uses the Jenkov Java concurrency tutorial as a topic checklist, then updates the material for modern Java:
| Area | Topics covered |
|---|---|
| Foundations | benefits, costs, concurrency models, same-threading, single-threaded concurrency, concurrency vs parallelism |
| Threads | creation, lifecycle, interruption, platform threads, virtual threads |
| Correctness | races, critical sections, thread safety, immutability, Java Memory Model, happens-before |
| Visibility | synchronized, volatile, cache coherence, false sharing, safe publication |
| Coordination | signaling, locks, conditions, read/write locks, semaphores, blocking queues, producer-consumer |
| Failure modes | deadlock, prevention, starvation, fairness, nested monitor lockout, slipped conditions, reentrance lockout, congestion |
| Execution | thread pools, executors, CompletableFuture, structured concurrency |
| Non-blocking | compare-and-swap, atomics, synchronizer anatomy, non-blocking algorithms |
| Performance | Amdahl's Law, contention, backpressure, observability, stress testing |
The Approach
1. Concurrency, Parallelism, and the Cost Model
Concurrency means multiple tasks can make progress during overlapping time windows. A single CPU core can run concurrent tasks by switching between them.
Parallelism means multiple tasks execute at the same instant on different cores. Parallelism requires hardware capacity and work that can be divided safely.
Multithreading can improve:
- utilization while one task waits for I/O,
- throughput across multiple cores,
- responsiveness by moving blocking work away from latency-sensitive threads,
- fairness by preventing one long task from monopolizing progress.
It also introduces costs:
- thread creation and stack memory,
- context switching and scheduler overhead,
- cache misses and cache-line traffic,
- synchronization and contention,
- harder debugging, testing, and failure recovery.
2. Concurrency Models
Shared-state concurrency
Multiple threads read and mutate the same objects. This model is flexible but requires synchronization around every invariant.
public final class Inventory {
private final Map<String, Integer> quantities = new HashMap<>();
public synchronized void reserve(String sku, int requested) {
int available = quantities.getOrDefault(sku, 0);
if (available < requested) {
throw new IllegalStateException("insufficient inventory");
}
quantities.put(sku, available - requested);
}
}
The lock protects the full read-check-write invariant. Replacing HashMap with ConcurrentHashMap would not make that sequence atomic.
Separate-state or shared-nothing concurrency
Each worker owns its mutable state and communicates through messages. Actor systems, event loops, and partitioned stream processors follow this model.
The benefit is fewer shared-memory races. The cost is explicit message protocols, queues, serialization boundaries, and eventual consistency.
Same-threading and single-threaded concurrency
Same-threading keeps related operations on one thread so no lock is needed for local state. Event-loop systems use a single thread per partition or loop while overlapping I/O through callbacks or non-blocking APIs.
This removes many data races but creates a strict rule: never block the event-loop thread. One slow operation delays every task behind it.
3. Creating Threads and Managing Their Lifecycle
Prefer executors for application work, but understand the underlying lifecycle:
NEW
└─ start() ─▶ RUNNABLE
├─ waiting for monitor ─▶ BLOCKED
├─ wait(), join(), park() ─▶ WAITING
├─ sleep(), timed wait ─▶ TIMED_WAITING
└─ run() completes ─▶ TERMINATED
Create a named platform thread when thread identity or lifecycle is meaningful:
Thread worker = Thread.ofPlatform()
.name("inventory-reconciler")
.daemon(false)
.start(() -> reconcileUntilInterrupted());
Use Runnable for no-result tasks and Callable<T> when a task returns a value or throws a checked exception.
Interruption is cooperative cancellation
interrupt() does not forcibly kill a thread. It requests cancellation:
void reconcileUntilInterrupted() {
try {
while (!Thread.currentThread().isInterrupted()) {
reconcileBatch();
Thread.sleep(250);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} finally {
closeResources();
}
}
Rules:
- propagate
InterruptedExceptionwhen the API allows it, - otherwise restore the interrupt flag with
Thread.currentThread().interrupt(), - do not swallow interruption and continue as if nothing happened,
- design blocking operations with cancellation and deadlines.
4. Platform Threads and Virtual Threads
Platform threads are backed one-to-one by operating-system threads and are relatively expensive. Virtual threads are scheduled by the JVM over a smaller set of carrier threads.
Virtual threads are designed for high-throughput workloads containing many blocking tasks:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Customer> customer =
executor.submit(() -> customerClient.fetch(customerId));
Future<List<Order>> orders =
executor.submit(() -> orderClient.fetchForCustomer(customerId));
return new Dashboard(customer.get(), orders.get());
}
Virtual threads increase the number of tasks that can wait efficiently. They do not make CPU work faster, increase database connections, or remove the need for timeouts and backpressure.
Do not pool virtual threads. Create one virtual thread per task. To protect a scarce dependency, use a concurrency limiter:
public final class LimitedCatalogClient {
private final Semaphore permits = new Semaphore(40);
private final CatalogClient delegate;
public Product fetch(String id) throws InterruptedException {
permits.acquire();
try {
return delegate.fetch(id);
} finally {
permits.release();
}
}
}
Modern Java notes:
- Virtual threads became final in Java 21.
- Java 24 removed nearly all virtual-thread pinning caused by blocking inside
synchronized. - Native calls and some library behavior can still pin or monopolize carriers, so observe rather than assume.
- Thread-local values work with virtual threads, but millions of copied or retained values can become expensive.
5. Race Conditions, Data Races, and Critical Sections
A data race exists when two threads access the same memory without a happens-before relationship and at least one access is a write.
A race condition is a higher-level correctness bug where the result depends on timing.
// Individual calls are thread-safe; the compound action is not.
if (!cache.containsKey(key)) {
cache.put(key, load(key));
}
// Atomic compound operation.
cache.computeIfAbsent(key, this::load);
A critical section should protect the smallest operation that preserves the invariant. A critical section that includes network calls, disk access, callbacks, or logging can turn correctness into a throughput problem.
6. Thread Safety, Immutability, and Safe Publication
Prefer these strategies in order:
- no shared mutable state,
- immutable values,
- thread confinement,
- concurrent collections and coordination utilities,
- explicit locking,
- custom lock-free algorithms only with evidence.
Immutable objects are naturally thread-safe when construction does not leak this:
public record RoutingTable(Map<String, URI> routes) {
public RoutingTable {
routes = Map.copyOf(routes);
}
}
Safe publication establishes a happens-before edge between construction and use. Common mechanisms include:
- static initialization,
- final fields after proper construction,
- storing into a
volatilefield, - publishing through a lock,
- publishing through a concurrent collection,
- completing a
Future.
public final class ConfigRegistry {
private volatile RoutingTable current = new RoutingTable(Map.of());
public RoutingTable current() {
return current;
}
public void replace(RoutingTable next) {
current = next;
}
}
The immutable snapshot plus volatile reference gives readers a consistent configuration without a read lock.
7. Java Memory Model and Happens-Before
The Java Memory Model defines which writes a read is allowed to observe. It is not simply "read from main memory" versus "read from CPU cache." Compilers, CPUs, registers, store buffers, and caches can all reorder or delay observations as long as single-thread semantics remain valid.
Three properties matter:
- Atomicity — does the operation happen indivisibly?
- Visibility — when does another thread observe the write?
- Ordering — which operations may be reordered?
Important happens-before relationships:
- program order within one thread,
- monitor unlock before a later lock of the same monitor,
- volatile write before a later read of the same field,
- actions before
Thread.start()before actions in the started thread, - actions in a thread before another thread returns from
join(), - task actions before
Future.get()returns the result, - final-field guarantees after safe construction.
8. synchronized, volatile, and Atomic Variables
| Tool | Mutual exclusion | Visibility | Compound atomicity | Typical use |
|---|---|---|---|---|
synchronized | yes | yes | yes, inside monitor | protect related mutable fields |
volatile | no | yes | no | state flag or immutable snapshot reference |
| atomic class | per operation | yes | selected operations | counters, CAS state transitions |
Lock | yes | yes | yes | timed, interruptible, or multi-condition locking |
volatile is appropriate for independent state:
private volatile boolean acceptingTraffic = true;
It is not enough for read-modify-write:
private volatile int requests;
void recordRequest() {
requests++; // read + add + write; increments can be lost
}
Use AtomicLong, LongAdder, or a lock depending on the invariant:
private final LongAdder requests = new LongAdder();
void recordRequest() {
requests.increment();
}
LongAdder scales under heavy contention by spreading updates across cells, but sum() is not an atomic snapshot relative to concurrent increments.
9. CPU Cache Coherence and False Sharing
Hardware cache coherence keeps cores from permanently disagreeing about a cache line, but it does not create Java-level happens-before relationships.
False sharing occurs when independent hot fields occupy the same cache line. Two cores repeatedly invalidate the line even though they update different variables.
Symptoms:
- throughput collapses as thread count increases,
- CPU usage is high but useful work is low,
- profiling shows cache-coherence traffic or contention without an obvious lock.
Mitigations:
- partition counters by thread or shard,
- use
LongAdderfor highly contended counters, - avoid arrays of adjacent hot mutable counters,
- consider
@Contendedonly in controlled environments because it is internal and adds memory overhead, - validate with JMH and hardware performance counters.
10. Intrinsic Locks and Explicit Locks
Use synchronized when it expresses the invariant clearly. It is automatically released and is well optimized by modern JVMs.
Use ReentrantLock when the design needs:
- interruptible lock acquisition,
- timed
tryLock, - multiple
Conditionwait sets, - optional fairness,
- non-block-structured acquisition.
public final class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Object[] items;
private int putIndex;
private int takeIndex;
private int size;
public BoundedBuffer(int capacity) {
this.items = new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lockInterruptibly();
try {
while (size == items.length) {
notFull.await();
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
size++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (size == 0) {
notEmpty.await();
}
T item = (T) items[takeIndex];
items[takeIndex] = null;
takeIndex = (takeIndex + 1) % items.length;
size--;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
Always wait in a while loop. Wakeups can be spurious, and another thread may consume the condition before the awakened thread reacquires the lock.
11. Read/Write Locks and Optimistic Reads
ReentrantReadWriteLock allows concurrent readers while excluding writers. It helps only when:
- reads dominate,
- read sections are not trivial,
- write frequency is low,
- contention is measurable.
For short critical sections, the bookkeeping can cost more than a normal lock.
StampedLock supports optimistic reads:
public Point snapshot() {
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return new Point(currentX, currentY);
}
StampedLock is not reentrant. Its stamps must be managed precisely, making it a specialized optimization rather than a default.
12. Thread Signaling, Conditions, and Slipped Conditions
Intrinsic signaling uses wait, notify, and notifyAll. The calling thread must own the object's monitor.
synchronized (monitor) {
while (!ready) {
monitor.wait();
}
consume();
}
Prefer higher-level constructs such as BlockingQueue, CountDownLatch, Semaphore, Phaser, or Condition.
A slipped condition happens when a thread checks a condition, loses exclusivity, and acts after the condition is no longer true. Check the condition and perform the state transition under the same lock.
13. Semaphores, Latches, Barriers, and Phasers
Use the primitive that matches the coordination shape:
| Primitive | Shape | Example |
|---|---|---|
Semaphore | limit simultaneous access | protect 40 database connections |
CountDownLatch | one-time wait for N events | wait for startup dependencies |
CyclicBarrier | reusable group rendezvous | iterative parallel algorithm |
Phaser | dynamic multi-phase coordination | workers join and leave phases |
Do not use a thread pool merely to limit access to a dependency. A semaphore expresses the actual constraint.
14. Blocking Queues and Producer-Consumer
A bounded BlockingQueue combines safe handoff with backpressure:
BlockingQueue<Job> queue = new ArrayBlockingQueue<>(1_000);
void submit(Job job) throws InterruptedException {
queue.put(job);
}
void consume() {
while (!Thread.currentThread().isInterrupted()) {
try {
Job job = queue.take();
process(job);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
}
Choose behavior intentionally:
putwaits for capacity,offerfails immediately,- timed
offercreates a deadline, - unbounded queues hide overload until memory or latency collapses.
The queue capacity is an operational policy. Size it from acceptable waiting time, service rate, burst size, and memory per item.
15. Thread Pools, Executors, and Backpressure
For CPU-bound tasks, start near the number of available processors and measure:
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = new ThreadPoolExecutor(
processors,
processors,
0L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(500),
Thread.ofPlatform().name("pricing-", 0).factory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
The executor policy is part of the API:
- pool size controls active work,
- queue capacity controls waiting work,
- rejection policy controls overload behavior,
- task deadlines control obsolete work,
- shutdown behavior controls deployment and process termination.
CallerRunsPolicy slows producers by making the submitting thread execute rejected work. It can provide backpressure, but it is dangerous if the caller is an event-loop, UI, or request-acceptor thread.
For blocking platform-thread workloads, a rough sizing model is:
threads ≈ cores × target utilization × (1 + wait time / compute time)
Treat the formula as a starting hypothesis, not a substitute for load tests.
16. Thread Congestion
Thread congestion occurs when runnable or blocked work exceeds the system's useful capacity.
Common causes:
- too many platform threads,
- unbounded queues,
- a small hot lock,
- downstream timeouts longer than user deadlines,
- tasks that submit dependent work back into the same saturated pool,
- blocking operations on event-loop threads.
Monitor:
- active threads and pool saturation,
- queue depth and oldest-task age,
- rejected-task rate,
- lock contention and blocked time,
- downstream connection-pool wait,
- p50, p95, and p99 latency together.
17. CompletableFuture Pipelines
CompletableFuture is useful for explicit asynchronous composition, especially when integrating APIs already expressed as stages.
public CompletableFuture<Dashboard> loadDashboard(String customerId) {
CompletableFuture<Customer> customer =
CompletableFuture.supplyAsync(
() -> customerClient.fetch(customerId),
blockingExecutor
);
CompletableFuture<List<Order>> orders =
CompletableFuture.supplyAsync(
() -> orderClient.fetchForCustomer(customerId),
blockingExecutor
);
return customer.thenCombine(orders, Dashboard::new)
.orTimeout(800, TimeUnit.MILLISECONDS)
.whenComplete((result, error) -> {
if (error != null) {
metrics.increment("dashboard.failure");
}
});
}
Key distinctions:
thenApplymapsT -> U,thenComposeflattensT -> CompletionStage<U>,thenCombinejoins independent stages,allOfwaits but discards typed results,exceptionallyrecovers with a value,whenCompleteobserves without changing the result.
Avoid the common pool for blocking production work. Pass an executor whose ownership, capacity, and shutdown policy are explicit.
18. Structured Concurrency
Structured concurrency makes child tasks part of a lexical scope. The parent cannot accidentally finish while children continue unobserved.
Java 25 exposes structured concurrency as a preview API:
// Compile and run with --enable-preview on Java 25.
try (var scope = StructuredTaskScope.open()) {
Subtask<Customer> customer =
scope.fork(() -> customerClient.fetch(customerId));
Subtask<List<Order>> orders =
scope.fork(() -> orderClient.fetchForCustomer(customerId));
scope.join();
return new Dashboard(customer.get(), orders.get());
}
The model improves:
- cancellation when one subtask fails,
- deadline propagation,
- exception aggregation,
- thread-dump observability,
- reasoning about task lifetime.
Because the API is preview in Java 25, isolate it behind application boundaries and confirm the exact API for the selected JDK.
19. Thread-Local and Scoped Context
ThreadLocal associates data with a thread:
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
void handle(Request request) {
TRACE_ID.set(request.traceId());
try {
process(request);
} finally {
TRACE_ID.remove();
}
}
Always remove values in pooled platform threads or request data can leak into the next task.
Thread-local context becomes less attractive with very large numbers of virtual threads. Java 25 scoped values provide immutable, bounded-lifetime context:
private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
ScopedValue.where(TRACE_ID, request.traceId())
.run(() -> process(request));
Prefer explicit parameters for ordinary business data. Use scoped context for cross-cutting values such as trace identity or security context when parameter threading would obscure the API.
20. Deadlock and Prevention
Deadlock requires four conditions:
- mutual exclusion,
- hold and wait,
- no preemption,
- circular wait.
Break at least one:
- enforce a global lock order,
- avoid nested locks,
- use timed
tryLockwith rollback, - never invoke unknown callbacks while holding a lock,
- reduce shared mutable state,
- move resource coordination into a queue or owner thread.
void transfer(Account left, Account right, Money amount)
throws InterruptedException {
Account first = left.id().compareTo(right.id()) < 0 ? left : right;
Account second = first == left ? right : left;
first.lock().lockInterruptibly();
try {
second.lock().lockInterruptibly();
try {
left.debit(amount);
right.credit(amount);
} finally {
second.lock().unlock();
}
} finally {
first.lock().unlock();
}
}
Detect deadlocks with:
jcmd <pid> Thread.print -l
jstack <pid>
Thread dumps show ownership and wait cycles. Capture multiple dumps several seconds apart to distinguish a deadlock from a slow critical section.
21. Starvation, Fairness, and Livelock
Starvation means a task never receives enough CPU time, lock access, queue capacity, or another required resource.
Fair locks approximate first-come-first-served acquisition but reduce throughput and still do not guarantee application-level fairness.
Livelock means threads remain active but repeatedly react to each other without completing. Retry loops with identical backoff can livelock.
Mitigations:
- bounded randomized backoff,
- priority aging,
- short critical sections,
- admission control,
- partitioning hot resources,
- fairness only where starvation risk justifies its cost.
22. Nested Monitor Lockout and Reentrance Lockout
Nested monitor lockout occurs when a thread waits while holding one lock that another thread needs before it can produce the signal.
Reentrance lockout appears in custom non-reentrant locks when a thread attempts to reacquire a lock it already owns.
Prefer:
- one lock per invariant,
Condition.await, which releases the associated lock while waiting,- established JDK synchronizers,
- lock-order documentation and assertions,
- avoiding custom lock implementations.
23. Compare-and-Swap and Atomic State Machines
Compare-and-swap updates a value only if it still equals the expected value:
public final class CircuitState {
private final AtomicReference<State> state =
new AtomicReference<>(State.CLOSED);
public boolean open() {
return state.compareAndSet(State.CLOSED, State.OPEN);
}
}
A CAS loop retries when another thread wins:
int increment(AtomicInteger value) {
while (true) {
int current = value.get();
int next = current + 1;
if (value.compareAndSet(current, next)) {
return next;
}
}
}
CAS avoids blocking but can waste CPU under contention.
The ABA problem
A value can change from A to B and back to A. A plain CAS sees A and assumes nothing changed. Use a version stamp when history matters:
AtomicStampedReference<Node> head =
new AtomicStampedReference<>(initial, 0);
24. Anatomy of a Synchronizer
Most synchronizers combine:
- an atomic state variable,
- a queue of waiting threads,
- rules for acquiring and releasing state,
- parking and unparking through
LockSupport.
AbstractQueuedSynchronizer provides this foundation for many JDK utilities. Extending it correctly requires precise ownership, cancellation, queue, and memory-ordering semantics. Application code should normally use existing synchronizers rather than implement new ones.
25. Non-Blocking Algorithms
Definitions:
- blocking — a delayed thread can prevent others from progressing,
- lock-free — the system as a whole keeps making progress,
- wait-free — every operation completes within a bounded number of steps.
Non-blocking algorithms trade lock management for retry loops, memory reclamation, ordering constraints, and harder proofs.
Use them when:
- profiling proves lock contention is material,
- a suitable JDK atomic or concurrent collection already exists,
- the team can test linearizability and failure behavior.
Do not replace a clear lock with custom CAS because "lock-free" sounds faster.
26. Amdahl's Law
If fraction P of a workload is parallelizable and N processors execute it, the theoretical speedup is:
speedup = 1 / ((1 - P) + P / N)
If 10% of the work is serial, unlimited processors cannot exceed 10× speedup. Real systems perform worse because of scheduling, coordination, memory bandwidth, skew, and contention.
Optimize the serial bottleneck and coordination path before adding threads.
27. Testing Concurrent Code
Normal unit tests rarely explore enough interleavings.
Use:
- deterministic tests for state transitions,
- time-bounded tests for cancellation and shutdown,
CountDownLatchor barriers to force specific interleavings,- repeated stress tests,
jcstressfor Java Memory Model outcomes,- JMH for performance comparisons,
- fault injection for timeout, rejection, and interruption paths.
Avoid tests that depend on Thread.sleep for ordering. They are slow and nondeterministic.
28. Production Diagnostics
Start with the symptom:
| Symptom | Evidence to collect |
|---|---|
| latency spike | thread dumps, queue age, downstream wait, lock contention |
| CPU spike | JFR CPU samples, runnable threads, retry loops, CAS failures |
| low CPU + low throughput | blocked threads, connection pools, external latency |
| memory growth | queue depth, retained thread-local values, task backlog |
| shutdown hangs | non-daemon threads, executor termination, blocked I/O |
Useful tools:
jcmd <pid> Thread.print -l
jcmd <pid> JFR.start name=concurrency settings=profile duration=60s filename=recording.jfr
jcmd <pid> VM.native_memory summary
Track application-level concurrency signals:
- executor active count,
- queue size and oldest-task age,
- rejection count,
- semaphore wait time,
- lock acquisition latency,
- virtual-thread count where supported,
- timeout and cancellation rate.
29. Decision Guide
| Need | Start with |
|---|---|
| publish immutable configuration | immutable object + volatile reference |
| atomic counter under moderate contention | AtomicLong |
| high-write metric counter | LongAdder |
| protect a multi-field invariant | synchronized |
| timed or interruptible lock acquisition | ReentrantLock |
| many readers, rare expensive writes | measure ReentrantReadWriteLock |
| producer-consumer handoff | bounded BlockingQueue |
| limit access to scarce dependency | Semaphore |
| wait for one-time startup events | CountDownLatch |
| fixed CPU-parallel computation | bounded platform-thread pool |
| many blocking I/O tasks | virtual thread per task |
| compose existing async APIs | CompletableFuture |
| parent-child task lifetime and cancellation | structured concurrency, when preview use is acceptable |
| thread-confined mutable context | explicit parameters, scoped values, or carefully cleared ThreadLocal |
30. Interview Checks
Why does volatile not make count++ safe?
count++ is a read-modify-write sequence. Volatile makes the reads and writes visible and ordered, but it does not combine them into one atomic action.
What is the difference between BLOCKED and WAITING?
BLOCKED means a platform thread is waiting to enter a synchronized monitor. WAITING means it voluntarily parked through operations such as wait, join, or LockSupport.park.
Why can a thread-safe collection still be used incorrectly?
Individual operations may be safe while a multi-operation invariant is not. Use compound methods such as computeIfAbsent, or protect the complete workflow.
When should virtual threads not be used?
They do not improve CPU-bound work and should not be pooled. They also require explicit protection for scarce downstream resources and careful observation of native blocking, retained thread-local data, and dependency capacity.
How should interruption be handled?
Propagate InterruptedException when possible. If it cannot be propagated, restore the interrupt flag and exit or translate the cancellation deliberately.
How do you prevent deadlock?
Keep one lock per invariant where possible, use a global acquisition order, avoid callbacks and I/O while locked, use timed acquisition when rollback is possible, and prefer higher-level coordination utilities.
Trade-offs
- Locks vs atomics: Locks express multi-field invariants clearly. Atomics work well for small state transitions but become difficult to reason about when several values must change together.
- Platform pools vs virtual threads: Pools manage scarce operating-system threads and are appropriate for CPU-bound work. Virtual threads simplify blocking I/O at high concurrency but can expose downstream bottlenecks faster.
- Fairness vs throughput: Fair scheduling reduces starvation risk but adds coordination and often lowers throughput.
- Shared state vs message passing: Shared state can be direct and low-latency. Message passing reduces races but adds queues, ownership boundaries, and possible eventual consistency.
- Read/write locks vs simple locks: Read/write locks help only under sustained read-heavy contention. Otherwise their bookkeeping and writer-starvation risk can make performance worse.
- Asynchronous stages vs structured tasks:
CompletableFutureintegrates well with stage-based APIs. Structured concurrency gives clearer task lifetime, cancellation, and observability but remains preview in Java 25. - Unbounded concurrency vs backpressure: More threads can increase throughput until a dependency saturates. Bounded queues, semaphores, deadlines, and rejection policies keep overload visible and recoverable.
References
- Jenkov Java Concurrency and Multithreading Tutorial — broad topic map and foundational explanations
- Java SE 25 Concurrency Guide — current platform overview
- JLS Chapter 17: Threads and Locks — Java Memory Model and happens-before rules
- JEP 444: Virtual Threads — final virtual-thread design delivered in Java 21
- JEP 491: Synchronize Virtual Threads without Pinning — Java 24 pinning improvements
- JEP 505: Structured Concurrency — Java 25 preview API
- Java SE 25 Scoped Values — bounded context propagation
- OpenJDK jcstress — Java concurrency stress-testing harness
- Java Concurrency in Practice — Brian Goetz et al.
- The Art of Multiprocessor Programming — Maurice Herlihy and Nir Shavit