Field note / java

publishedupdated 2026-07-24#java#concurrency#threads#memory-model#virtual-threads

Java Concurrency: From Memory Model to Virtual Threads

A production-focused guide to thread safety, the Java Memory Model, locks, coordination utilities, executors, virtual threads, structured concurrency, lock-free algorithms, and failure diagnosis.

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.concurrent utilities 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:

  1. Scheduling — which task runs on which thread?
  2. Memory semantics — when does one thread observe another thread's writes?
  3. Mutual exclusion — which operations must not overlap?
  4. Coordination — how do tasks wait, signal, hand off, cancel, and fail together?
  5. 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:

AreaTopics covered
Foundationsbenefits, costs, concurrency models, same-threading, single-threaded concurrency, concurrency vs parallelism
Threadscreation, lifecycle, interruption, platform threads, virtual threads
Correctnessraces, critical sections, thread safety, immutability, Java Memory Model, happens-before
Visibilitysynchronized, volatile, cache coherence, false sharing, safe publication
Coordinationsignaling, locks, conditions, read/write locks, semaphores, blocking queues, producer-consumer
Failure modesdeadlock, prevention, starvation, fairness, nested monitor lockout, slipped conditions, reentrance lockout, congestion
Executionthread pools, executors, CompletableFuture, structured concurrency
Non-blockingcompare-and-swap, atomics, synchronizer anatomy, non-blocking algorithms
PerformanceAmdahl'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.
Start with the workload shape: virtual threads scale blocking tasks, while bounded pools protect CPU-bound work.

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 InterruptedException when 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());
}
caution

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:

  1. no shared mutable state,
  2. immutable values,
  3. thread confinement,
  4. concurrent collections and coordination utilities,
  5. explicit locking,
  6. 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 volatile field,
  • 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.
Publishing an immutable object through a volatile reference makes earlier construction writes visible to the consumer.

8. synchronized, volatile, and Atomic Variables

ToolMutual exclusionVisibilityCompound atomicityTypical use
synchronizedyesyesyes, inside monitorprotect related mutable fields
volatilenoyesnostate flag or immutable snapshot reference
atomic classper operationyesselected operationscounters, CAS state transitions
Lockyesyesyestimed, 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 LongAdder for highly contended counters,
  • avoid arrays of adjacent hot mutable counters,
  • consider @Contended only 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 Condition wait 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:

PrimitiveShapeExample
Semaphorelimit simultaneous accessprotect 40 database connections
CountDownLatchone-time wait for N eventswait for startup dependencies
CyclicBarrierreusable group rendezvousiterative parallel algorithm
Phaserdynamic multi-phase coordinationworkers 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:

  • put waits for capacity,
  • offer fails immediately,
  • timed offer creates 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:

  • thenApply maps T -> U,
  • thenCompose flattens T -> CompletionStage<U>,
  • thenCombine joins independent stages,
  • allOf waits but discards typed results,
  • exceptionally recovers with a value,
  • whenComplete observes 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:

  1. mutual exclusion,
  2. hold and wait,
  3. no preemption,
  4. circular wait.

Break at least one:

  • enforce a global lock order,
  • avoid nested locks,
  • use timed tryLock with 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:

  1. an atomic state variable,
  2. a queue of waiting threads,
  3. rules for acquiring and releasing state,
  4. 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,
  • CountDownLatch or barriers to force specific interleavings,
  • repeated stress tests,
  • jcstress for 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:

SymptomEvidence to collect
latency spikethread dumps, queue age, downstream wait, lock contention
CPU spikeJFR CPU samples, runnable threads, retry loops, CAS failures
low CPU + low throughputblocked threads, connection pools, external latency
memory growthqueue depth, retained thread-local values, task backlog
shutdown hangsnon-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

NeedStart with
publish immutable configurationimmutable object + volatile reference
atomic counter under moderate contentionAtomicLong
high-write metric counterLongAdder
protect a multi-field invariantsynchronized
timed or interruptible lock acquisitionReentrantLock
many readers, rare expensive writesmeasure ReentrantReadWriteLock
producer-consumer handoffbounded BlockingQueue
limit access to scarce dependencySemaphore
wait for one-time startup eventsCountDownLatch
fixed CPU-parallel computationbounded platform-thread pool
many blocking I/O tasksvirtual thread per task
compose existing async APIsCompletableFuture
parent-child task lifetime and cancellationstructured concurrency, when preview use is acceptable
thread-confined mutable contextexplicit 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: CompletableFuture integrates 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