Lesson 69 of 71
Concurrency, Memory Model, and Lock-Free Code
Short answers for interview questions on sections 7 to 10 of the course: concurrency, CPU and memory hardware, the Java Memory Model, and atomics and lock-free code. Each group links to the lessons that teach the topic in depth. Code blocks run on JDK 25; racy code is shown compile-only with the explanation.
| Group | Lessons |
|---|---|
| Threads and monitors | Threads, synchronized, wait and notify |
| Locks and synchronizers | Locks and Synchronizers |
| Executors and futures | Executors and CompletableFuture |
| Concurrent collections | Concurrent Collections |
| Virtual threads | Virtual Threads in Depth |
| Hardware | CPU Caches and Locality, Cache Coherence, False Sharing, Branch Prediction, and NUMA |
| Memory model | Visibility and Reordering, Happens-Before, Safe Publication |
| Atomics and lock-free | Atomics and CAS, VarHandle and Fences, Lock-Free Queues, The Disruptor Pattern |
Threads and monitors
1. What is the difference between start() and run()?
start() creates a new thread that calls run(). Calling run() directly executes the body on the caller's thread. A Thread can be started once; a second start() throws IllegalThreadStateException.
2. Name the thread states. In which state is a thread blocked on a socket read?
NEW, RUNNABLE, BLOCKED (waiting to enter a monitor), WAITING, TIMED_WAITING, TERMINATED. A thread in blocking socket IO reports RUNNABLE, because the JVM does not track OS-level waits.
3. How does interruption work, and how should InterruptedException be handled?
Interruption is cooperative: interrupt() sets a flag. Blocking methods such as sleep, wait, and join throw InterruptedException and clear the flag. The handler either rethrows it or restores the flag with Thread.currentThread().interrupt(). Swallowing it hides the cancellation request from callers.
4. What does synchronized guarantee?
Mutual exclusion and visibility. Unlocking a monitor happens-before every later lock of the same monitor, so writes made inside the block are visible to the next thread that enters it. Intrinsic locks are reentrant: the owner thread can enter again, and a hold count tracks the depth.
5. Why must wait() be called in a while loop?
Two reasons: spurious wakeups are allowed, and another thread may consume the condition between the notify and the wakeup. The loop rechecks the predicate after every return. wait must also be called while holding the monitor of the same object, or it throws IllegalMonitorStateException.
6. notify or notifyAll?
notifyAll unless every waiter waits for the same condition and any one of them can make progress. With notify, a producer can wake another producer while consumers sleep, and the system stalls.
7. Follow-up: is biased locking still a factor?
No. It was disabled by default and deprecated in Java 15 (JEP 374) and removed in Java 18.
8. What are the four conditions for deadlock, and which one is easiest to break?
Mutual exclusion, hold and wait, no preemption, and circular wait. A global lock order (for example, lock accounts by ascending id) breaks circular wait. Timed tryLock breaks hold and wait. ThreadMXBean.findDeadlockedThreads and jstack detect a deadlock that has happened.
9. Spot the bug.
public class Transfer {
static class Account {
final int id;
long balance;
Account(int id, long balance) { this.id = id; this.balance = balance; }
}
static void transfer(Account from, Account to, long amount) {
synchronized (from) {
synchronized (to) {
from.balance -= amount;
to.balance += amount;
}
}
}
public static void main(String[] args) {
Account a = new Account(1, 100), b = new Account(2, 100);
new Thread(() -> transfer(a, b, 10)).start();
new Thread(() -> transfer(b, a, 10)).start();
}
}Thread 1 locks a then waits for b; thread 2 locks b then waits for a. That is a circular wait and can deadlock. The fix orders the locks by id:
public class Transfer {
static class Account {
final int id;
long balance;
Account(int id, long balance) { this.id = id; this.balance = balance; }
}
static void transfer(Account from, Account to, long amount) {
Account first = from.id < to.id ? from : to;
Account second = first == from ? to : from;
synchronized (first) {
synchronized (second) {
from.balance -= amount;
to.balance += amount;
}
}
}
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1, 100), b = new Account(2, 100);
Thread t1 = new Thread(() -> { for (int i = 0; i < 10_000; i++) transfer(a, b, 1); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10_000; i++) transfer(b, a, 1); });
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println("a=" + a.balance + " b=" + b.balance + " total=" + (a.balance + b.balance));
}
}a=100 b=100 total=200Locks and synchronizers
10. What does ReentrantLock add over synchronized?
tryLock (immediate and timed), lockInterruptibly, an optional fair mode, and several Condition objects per lock. The cost is that unlock() must sit in a finally block; a missed unlock is never released.
11. Fair or unfair lock?
Unfair (the default) lets an arriving thread barge ahead of queued ones, which avoids a context switch and gives higher throughput. Fair mode hands the lock in FIFO order, reduces starvation, and costs throughput.
12. Can a ReentrantReadWriteLock read lock be upgraded to a write lock?
No. Downgrade (hold write, take read, release write) is allowed. An upgrade attempt blocks forever, because the write lock waits for all readers, including the thread itself.
13. How does a StampedLock optimistic read work?
tryOptimisticRead() returns a stamp without taking a lock. The reader copies fields into locals, then calls validate(stamp). If a writer acquired the lock in between, validation fails and the reader falls back to readLock(). StampedLock is not reentrant and has no conditions.
14. What is AQS?
AbstractQueuedSynchronizer is a volatile int state, CAS on that state, and a FIFO queue of parked threads. Subclasses define what state means: hold count for ReentrantLock, permits for Semaphore, remaining count for CountDownLatch.
15. CountDownLatch, CyclicBarrier, Phaser, Semaphore: when to use each?
| Tool | Waits for | Reusable | Notes |
|---|---|---|---|
CountDownLatch |
N events | No | Any thread may count down |
CyclicBarrier |
N peer threads | Yes | Breaks for all if one party fails or times out |
Phaser |
Dynamic set of parties, per phase | Yes | Parties register and deregister |
Semaphore |
A permit | Yes | No owner; any thread may release |
16. Coding: write a bounded blocking queue.
One lock with two conditions, notFull and notEmpty, each awaited in a while loop:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedQueue {
static final class Bounded<T> {
private final Object[] items;
private int head, tail, count;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
Bounded(int capacity) { items = new Object[capacity]; }
void put(T x) throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == items.length) notFull.await();
items[tail] = x;
tail = (tail + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
@SuppressWarnings("unchecked")
T take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (count == 0) notEmpty.await();
T x = (T) items[head];
items[head] = null;
head = (head + 1) % items.length;
count--;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Bounded<Integer> q = new Bounded<>(4);
long[] sum = new long[1];
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 100_000; i++) sum[0] += q.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
consumer.start();
for (int i = 1; i <= 100_000; i++) q.put(i);
consumer.join();
System.out.println("sum=" + sum[0]);
}
}sum=5000050000signal (not signalAll) is correct here because each condition has one kind of waiter. join() makes sum[0] visible to main. Follow-up: ArrayBlockingQueue uses this design with one lock; LinkedBlockingQueue uses separate put and take locks so producers and consumers do not contend.
Executors and CompletableFuture
17. In what order does a ThreadPoolExecutor handle a new task?
Start a core thread if fewer than core size run; otherwise offer to the queue; if the queue is full, start a thread up to max size; otherwise reject. Consequence: with an unbounded queue the pool never grows past core size, and memory is the only limit. Executors.newFixedThreadPool has this property.
18. Which rejection policy gives back-pressure?
CallerRunsPolicy. The submitting thread runs the task itself, so it stops submitting until the task finishes.
19. How large should a pool be?
CPU-bound: about the number of cores. IO-bound: cores * (1 + wait / compute), capped by what the downstream system (database pool, remote service) can accept.
20. How does ForkJoinPool balance work?
Each worker has a deque. It pushes and pops its own tasks LIFO and steals from other deques FIFO, which takes the oldest and usually largest task. Blocking inside a ForkJoinPool task starves the pool.
21. thenApply, thenCompose, thenCombine, allOf?
thenApply maps a value; thenCompose flat-maps a function that returns a CompletableFuture; thenCombine joins two independent futures; allOf completes when all given futures complete.
22. How do errors propagate in a CompletableFuture chain?
A failure skips later value stages until a handler: exceptionally (recover), handle (value or error), or whenComplete (observe). Dependent stages receive the cause wrapped in CompletionException; Future.get wraps it in ExecutionException.
23. Scenario: a service uses supplyAsync(() -> jdbcCall()) and latency spikes under load. Why?
Without an executor argument, async stages run on ForkJoinPool.commonPool(), sized for CPU work. Blocking JDBC calls occupy its few threads, and every other user of the common pool (parallel streams, other futures) waits. Pass a dedicated executor sized for the IO.
24. Does cancel(true) on a CompletableFuture interrupt the running task?
No. It completes the future with a CancellationException; the computation keeps running. shutdownNow on an executor does interrupt worker threads, and returns tasks still in the queue.
Concurrent collections
25. Why is Collections.synchronizedMap not enough for a cache?
Each call is atomic, but check-then-act sequences (if (!m.containsKey(k)) m.put(k, v)) are not. Use ConcurrentHashMap.computeIfAbsent, putIfAbsent, or merge.
26. How is ConcurrentHashMap built in Java 8 and later?
CAS installs the first node in an empty bin; otherwise the writer locks the bin head with synchronized. Reads take no lock. Resizing is cooperative: writing threads help transfer bins. A bin treeifies at 8 nodes when the table has at least 64 slots, and untreeifies at 6.
27. Follow-up: what are the rules for compute and merge?
They are atomic per key and run under the bin lock. The function must be short and must not modify the same map, because it holds the lock and can block other keys in the bin.
28. Is size() exact?
It sums baseCount and the CounterCell array, so during concurrent updates it is an estimate. mappingCount() returns a long and is preferred for large maps.
29. Why does ConcurrentHashMap reject null?
So that get returning null always means absent. With null values a caller would need a second call to distinguish, and the two calls would race.
30. When does CopyOnWriteArrayList fit?
Read-mostly lists such as listener lists. Every write copies the array (O(n)); iterators see a snapshot and never throw ConcurrentModificationException.
31. ArrayBlockingQueue or LinkedBlockingQueue?
ArrayBlockingQueue is bounded, preallocated, and uses one lock. LinkedBlockingQueue uses two locks and allocates a node per element; its default capacity is Integer.MAX_VALUE, so it is unbounded unless a capacity is given. Other members: SynchronousQueue (no capacity, hand-off), DelayQueue (releases after a delay), PriorityBlockingQueue.
32. What is the sorted concurrent map, and what is the cost of ConcurrentLinkedQueue.size()?
ConcurrentSkipListMap. ConcurrentLinkedQueue.size() traverses the queue, O(n), and is not exact under concurrent changes.
33. Scenario: how does a multi-stage pipeline shut down cleanly?
Bounded queues between stages give back-pressure. Each stage, after its input is drained, sends one poison pill per downstream consumer, so every consumer receives exactly one stop signal.
Virtual threads
34. What is a virtual thread?
A java.lang.Thread (final in Java 21, JEP 444) scheduled by the JDK instead of the OS. Its stack is stored on the heap as a continuation. It runs on a carrier (platform) thread only while it has work; blocking in a JDK-adapted operation unmounts it and frees the carrier.
35. What scheduler runs virtual threads?
A dedicated FIFO ForkJoinPool, separate from the common pool, with parallelism equal to the number of available processors by default.
36. What is pinning, and what changed in Java 24?
Pinning means the virtual thread blocks while holding its carrier. Before Java 24, blocking inside synchronized or Object.wait pinned. JEP 491 (Java 24) removed that case. Native frames and class initialization still pin. File IO pins too, and the scheduler compensates by adding a carrier temporarily. Pinning is observed with the JFR event jdk.VirtualThreadPinned; the system property jdk.tracePinnedThreads was removed in Java 24.
37. Should virtual threads be pooled?
No. They are cheap to create; create one per task with Executors.newVirtualThreadPerTaskExecutor(). To limit load on a scarce resource (database connections, a rate-limited API), guard the resource with a Semaphore.
38. What is the problem with ThreadLocal caches on virtual threads?
A cache stored per thread is rebuilt for each of millions of short-lived threads, so it neither saves work nor memory. Scoped values (final in Java 25) pass immutable context to a bounded call tree. Structured concurrency is still a preview API in Java 25 (JEP 505).
39. Scenario: a trading engine's matching thread is CPU-bound. Will virtual threads lower its latency?
No. Virtual threads raise throughput of tasks that wait. CPU-bound and ultra-low-latency code gains nothing and adds scheduling indirection; such code uses dedicated platform threads, often pinned to cores.
CPU and memory hardware
40. What is a cache line and why does it matter for Java code?
The unit caches load and evict: 64 bytes on x86, holding 16 int or 8 long values. Memory cost is counted in lines, not fields. An L1 hit is about 1 ns and main memory about 100 ns; the ratio drives design.
41. Spatial and temporal locality?
Spatial: use bytes next to ones just used (sequential array access). Temporal: reuse data before it is evicted. A working set slightly larger than a cache level causes a sharp drop in hit rate.
42. Why is iterating int[][] m by m[j][i] slower than m[i][j]?
A Java int[][] is an array of row arrays. Varying the last index in the inner loop walks one row sequentially; varying the first jumps to a different row array, and in a large matrix each access misses the cache and the TLB.
43. Arrays of objects or parallel arrays?
For loops over one or two fields of many elements, parallel arrays (structure of arrays) put only the needed data in each line. An array of objects holds references, so each element is a pointer to follow plus header bytes.
44. Why are linked lists slow to traverse?
Pointer chasing: the address of the next node is known only after the current load completes, so misses are dependent and the hardware prefetcher, which detects sequential and constant-stride patterns, cannot hide them.
45. What is the TLB?
A cache of virtual-to-physical page translations. Large pages cover more memory per entry and reduce TLB misses for big heaps. Loop blocking (tiling) keeps a working set within cache and TLB reach.
46. Describe MESI.
Each line in each cache is Modified, Exclusive, Shared, or Invalid. A write needs the line in M (or E, which upgrades silently), which invalidates every other copy. Coherence is per line; it does not order accesses to different lines.
47. Follow-up: if caches are coherent, why can another core observe reordering?
Store buffers and invalidation queues. A store waits in the core's store buffer while later loads complete, so a later load can pass an earlier store. On x86 this store-load reordering is the only one allowed; ARM also reorders load-load and store-store.
48. What is false sharing and how is it fixed?
Contention on a cache line, not on a variable: two threads write independent fields that share a line, and the line moves between cores on every write. Fixes: padding to 64 or 128 bytes, or per-thread data. The JVM chooses field order inside a class, so manual padding uses a class hierarchy. @jdk.internal.vm.annotation.Contended pads automatically but needs --add-exports to compile and -XX:-RestrictContended to take effect outside the JDK; LongAdder uses it internally.
49. Why can sorting data make a loop faster without changing the work?
Branch prediction. A data-dependent branch on random data is mispredicted about half the time, costing about 15 to 20 cycles each. Sorted data makes the branch predictable. Branchless code (arithmetic or conditional moves) removes the branch.
50. What do NUMA and context switches mean for a low-latency process?
On NUMA machines memory is placed on the node of the first thread that touches it; remote memory is slower. Pin threads and their memory to one node. UseNUMA is off by default. A context switch costs microseconds directly and more through cold caches; busy-spinning avoids it at the cost of a full core.
Java Memory Model
51. Why does the JMM exist?
Caches, store buffers, and the JIT compiler reorder and cache memory operations. The JMM defines which values a read may return, so that code that follows its rules gives the same results on every CPU and JIT.
52. Name the three separate properties.
Atomicity (no lost updates), visibility (a write is seen by other threads), and ordering (writes are seen in program order). volatile gives visibility and ordering for single accesses, not atomicity of count++.
53. Spot the bug.
public class Stop {
static boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
long n = 0;
while (running) n++;
System.out.println(n);
});
worker.start();
Thread.sleep(100);
running = false;
worker.join();
}
}running is a plain field. The JIT may hoist the read out of the loop, and the worker never stops. Thread.sleep and Thread.onSpinWait have no memory effects, so neither helps. Declaring the field volatile (or reading it with getOpaque or stronger) fixes it.
54. What is happens-before?
A partial order of visibility, not of time. If action A happens-before B, B sees A's effects. Edges: program order within a thread, unlock to later lock of the same monitor, volatile write to later read of that variable, Thread.start() to the started thread's actions, a thread's actions to the return of join(), and transitivity. Two actions in different threads with no chain of edges are unordered.
55. What is a data race, and what does the JMM promise for race-free code?
Two conflicting accesses (same variable, at least one write) not ordered by happens-before. A program with no data races is sequentially consistent. A racy program may show reorderings, but never out-of-thin-air values.
56. How can plain fields be published through one volatile?
A volatile write publishes all earlier writes of the same thread to any thread that reads the volatile and sees the written value (piggybacking):
public class Piggyback {
static int a, b;
static volatile boolean ready;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!ready) Thread.onSpinWait();
System.out.println("a=" + a + " b=" + b);
});
reader.start();
a = 1;
b = 2;
ready = true;
reader.join();
}
}a=1 b=2The reader always prints a=1 b=2: the plain writes come before the volatile write in program order, and the volatile read that sees true comes after it.
57. Can a non-volatile long be torn?
Yes. JLS 17.7 allows a plain long or double write to be split into two 32-bit writes. Volatile long and double accesses are always atomic.
58. What makes publication safe?
An action with a happens-before edge between construction and use: static initialization, a volatile field, an AtomicReference, a lock, a concurrent collection, or final fields. Unsafe publication through a plain field can show default field values to another thread.
59. What do final fields guarantee?
A thread that sees a reference to an object sees the values its final fields had at the end of the constructor, even if the reference was published by a race. The guarantee holds only if this did not escape during construction. HotSpot implements it with a StoreStore barrier ("freeze") at the end of the constructor.
60. Spot the bug.
public class Listener {
private final int threshold;
public Listener(EventSource source) {
source.register(this);
this.threshold = 10;
}
void onEvent(int v) {
if (v > threshold) System.out.println("alert");
}
interface EventSource { void register(Listener l); }
}this escapes in the constructor. Another thread can call onEvent before threshold is set and read 0, and final semantics do not apply to an escaped object. The fix is a private constructor and a static factory that registers after construction. Starting a thread in a constructor has the same problem.
61. Spot the bug in double-checked locking.
public class Config {
private static Config instance;
private final java.util.Map<String, String> values = new java.util.HashMap<>();
static Config get() {
if (instance == null) {
synchronized (Config.class) {
if (instance == null) instance = new Config();
}
}
return instance;
}
}instance is not volatile. The first unsynchronized read has no happens-before edge to the write, so a reader can see a non-null reference to a partly constructed object. Declaring instance volatile fixes it.
62. Coding: write a thread-safe lazy singleton.
The holder-class idiom. The JVM initializes Holder on first access, under the class initialization lock, and class initialization happens-before any use of the class:
public class LazySingleton {
static final class Registry {
static int created;
private Registry() { created++; }
private static final class Holder {
static final Registry INSTANCE = new Registry();
}
static Registry get() { return Holder.INSTANCE; }
}
public static void main(String[] args) throws InterruptedException {
Registry[] seen = new Registry[8];
Thread[] ts = new Thread[8];
for (int i = 0; i < ts.length; i++) {
int k = i;
ts[i] = new Thread(() -> seen[k] = Registry.get());
ts[i].start();
}
for (Thread t : ts) t.join();
boolean same = true;
for (Registry r : seen) same &= r == seen[0];
System.out.println("created=" + Registry.created + " sameInstance=" + same);
}
}created=1 sameInstance=trueNo lock is taken after initialization. For an instance field (not static), double-checked locking with a volatile field is the equivalent.
63. Which memory barriers exist and which is expensive?
LoadLoad, StoreStore, LoadStore, StoreLoad. On x86 only StoreLoad needs an instruction (a locked instruction or mfence after a volatile store), because x86 reorders only store-then-load. ARM needs acquire and release instructions for volatile loads and stores.
64. Scenario: a config object is reloaded every minute and read on every request. Design the access.
Build an immutable snapshot (all fields final, defensive copies of collections), then publish it with one volatile write. Readers read the reference once into a local and use that local for the whole request, so they never mix two versions. No lock is needed.
Atomics and CAS
65. What does compare-and-set do, and what does a failure mean?
It writes the new value only if the current value equals the expected one, as one atomic step. A failure means another thread changed the value, so some thread made progress. Atomic get and set have volatile semantics; compareAndSet acts as a volatile read and write.
66. Coding: write a CAS counter by hand.
import java.util.concurrent.atomic.AtomicLong;
public class CasCounter {
static final AtomicLong value = new AtomicLong();
static long increment() {
while (true) {
long current = value.get();
long next = current + 1;
if (value.compareAndSet(current, next)) return next;
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[4];
for (int i = 0; i < ts.length; i++) {
ts[i] = new Thread(() -> { for (int j = 0; j < 250_000; j++) increment(); });
ts[i].start();
}
for (Thread t : ts) t.join();
System.out.println("count=" + value.get());
}
}count=1000000incrementAndGet does the same in one call. The loop shape is what interviews ask for, since it generalizes to any pure update function.
67. Follow-up: why must the lambda passed to updateAndGet be side-effect free?
Under contention the CAS fails and the function runs again with the new value. A side effect (logging, incrementing another counter) would happen once per attempt.
68. AtomicLong or LongAdder?
LongAdder for write-heavy counters: it stripes updates across padded cells, so threads do not fight over one line. sum() is not an atomic snapshot, and there is no CAS. AtomicLong when the exact current value or CAS is needed.
69. How are several fields updated atomically without a lock?
Put them in an immutable record and CAS an AtomicReference to a new record. compareAndSet compares references by identity, not with equals.
70. What is the ABA problem?
A thread reads A, another changes it to B and back to A, and the first thread's CAS succeeds although the state changed (for example, a node was freed and reused). AtomicStampedReference pairs the reference with an int stamp that increases on every change.
71. When are field updaters used?
AtomicIntegerFieldUpdater and friends update a volatile field of the exact type in place, so millions of instances do not each carry a separate AtomicInteger object.
72. Lock-free versus wait-free?
Lock-free: some thread always makes progress (a CAS loop). Wait-free: every thread completes in a bounded number of its own steps (a single getAndAdd on hardware with an atomic add instruction).
VarHandles and fences
73. What is a VarHandle?
A typed reference to a field, static field, or array element with access methods in several memory-ordering modes. The ordering is chosen per access, not per field declaration. Handles are created with MethodHandles.lookup().findVarHandle, findStaticVarHandle, or MethodHandles.arrayElementVarHandle, and stored in static final fields so the JIT can inline them. With FFM for off-heap memory, they replace sun.misc.Unsafe.
74. List the access modes from weakest to strongest.
| Mode | Guarantees |
|---|---|
| Plain | None beyond single-thread order; plain long may tear |
| Opaque | No hoisting, no tearing, coherent per variable; no ordering with other variables |
| Acquire/release | Release store orders earlier accesses before it; acquire load orders later accesses after it |
| Volatile | Acquire/release plus StoreLoad: a total order of all volatile accesses |
75. When is release/acquire not enough?
Dekker-style code: thread 1 writes x then reads y, thread 2 writes y then reads x. Release/acquire allows both reads to see 0, because it does not order a store before a later load of a different variable. Volatile mode or VarHandle.fullFence() is needed.
76. What is lazySet?
The older name for setRelease. On x86 it compiles to a plain store, while a volatile store adds a locked instruction. It is the publish step of single-writer queues.
77. What are releaseFence and acquireFence?
Explicit fences that order plain accesses: releaseFence before a store orders earlier accesses before it; acquireFence after a load orders later accesses after it.
78. Follow-up: what does signature polymorphism mean for a VarHandle call?
The types come from the call site. Arguments must convert to the field type (widening is allowed; passing a long to an int handle throws WrongMethodTypeException), and the result must be cast to the field type.
Lock-free queues and the Disruptor
79. Why use power-of-two capacity and unbounded sequences in a ring buffer?
seq & (capacity - 1) replaces seq % capacity. Head and tail are ever-growing long values; tail - head is the size, the queue is full when it equals the capacity and empty when head == tail.
80. What is the single-writer principle?
Each counter has exactly one writing thread. That writer updates it with setRelease, and readers use getAcquire, so no CAS is needed. An SPSC queue needs no StoreLoad: a stale view of the other side's counter only makes the queue look fuller or emptier, and the caller retries.
81. Coding: write an SPSC queue.
import java.util.concurrent.atomic.AtomicLong;
public class SpscQueue {
static final class Spsc<E> {
private final Object[] buffer;
private final int mask;
private final AtomicLong head = new AtomicLong(); // written by consumer only
private final AtomicLong tail = new AtomicLong(); // written by producer only
Spsc(int capacity) {
if (Integer.bitCount(capacity) != 1) throw new IllegalArgumentException("power of two");
buffer = new Object[capacity];
mask = capacity - 1;
}
boolean offer(E e) {
long t = tail.get();
if (t - head.getAcquire() == buffer.length) return false;
buffer[(int) (t & mask)] = e;
tail.setRelease(t + 1);
return true;
}
@SuppressWarnings("unchecked")
E poll() {
long h = head.get();
if (h == tail.getAcquire()) return null;
int i = (int) (h & mask);
E e = (E) buffer[i];
buffer[i] = null;
head.setRelease(h + 1);
return e;
}
}
public static void main(String[] args) throws InterruptedException {
Spsc<Integer> q = new Spsc<>(1024);
int n = 1_000_000;
long[] result = new long[2];
Thread consumer = new Thread(() -> {
long sum = 0, expected = 0, outOfOrder = 0;
for (int received = 0; received < n; ) {
Integer v = q.poll();
if (v == null) { Thread.onSpinWait(); continue; }
if (v != expected) outOfOrder++;
expected = v + 1;
sum += v;
received++;
}
result[0] = sum;
result[1] = outOfOrder;
});
consumer.start();
for (int i = 0; i < n; i++) {
while (!q.offer(i)) Thread.onSpinWait();
}
consumer.join();
System.out.println("sum=" + result[0] + " outOfOrder=" + result[1]);
}
}sum=499999500000 outOfOrder=0The producer writes the slot and then publishes tail with a release write; the consumer's acquire read of tail makes the slot write visible. Follow-ups: cache the last-read value of the other side's counter so cross-core reads happen about once per buffer length, and pad head and tail onto separate cache lines through a class hierarchy. JCTools SpscArrayQueue is the production version.
82. How does an MPSC queue differ?
Producers claim a slot by CAS on the tail, write the element, and publish with a release write to the slot itself. The consumer waits on the slot, so a producer stalled between claim and publish blocks the consumer at that slot. JCTools MpscArrayQueue follows this design.
83. Which wait strategy for an idle consumer?
Busy-spin gives the lowest latency and burns a core. Backoff strategies spin, then Thread.yield(), then park, trading latency for CPU.
84. Describe the Disruptor.
A preallocated ring of event objects, a producer cursor, and one sequence per consumer, each with a single writer. The protocol is next() to claim, write into the slot, publish() (a release write to the cursor). Consumers wait at a SequenceBarrier, which returns the minimum of the cursor and the sequences of upstream consumers; that is how stage dependencies (for example, a diamond) work without extra queues.
85. Follow-up: why is the Disruptor faster than ArrayBlockingQueue?
No lock; no allocation per message, because events are reused; batching, because a consumer that falls behind processes every available event and writes its sequence once; multicast to all consumers; and selectable wait strategies. Sequences are padded through superclasses so they sit on separate cache lines.
86. How does the producer avoid overwriting unread events?
It gates on the slowest end consumer: it may claim sequence n only when the minimum gating sequence is at least n - size. It caches that minimum, so it reads other cores' lines only when the cached value would block it. In single-producer mode no CAS is needed; in multi-producer mode slots are claimed with an atomic add and availability is tracked per slot.
87. Scenario: an order gateway uses a LinkedBlockingQueue between the network thread and the matcher. p99 latency is high and GC runs often. What changes?
LinkedBlockingQueue allocates a node per element, takes locks, and parks and wakes threads. Replace it with a bounded SPSC ring (JCTools SpscArrayQueue) or a Disruptor with preallocated events, run the matcher on a dedicated core with a busy-spin wait strategy, and pad hot counters. The trade-off is a core that runs at 100 percent.
Interview points
- Thread and monitor questions test the
while-loop rule forwait, cooperative interruption, and lock ordering for deadlock. - Pool questions test the
ThreadPoolExecutororder (core, queue, max, reject) and the common-pool trap. - Memory model answers name a specific happens-before edge; "it is visible eventually" is not an answer.
volatilegives visibility and ordering, not atomicity; compound actions need a lock or CAS.- Lock-free answers state the progress guarantee and the ABA risk.
- HFT answers connect code to hardware: cache lines, false sharing, store buffers, and the cost of StoreLoad.
Summary
- Bounded blocking queue: one lock, two conditions,
whileloops aroundawait. - Lazy singleton: holder class for statics,
volatiledouble-checked locking for instances. - CAS counter: read, compute,
compareAndSet, retry on failure. - SPSC queue: power-of-two ring, single-writer counters,
setReleaseandgetAcquire. - Safe publication uses static init,
volatile, atomics, locks, concurrent collections, orfinalfields. - Release/acquire is enough for message passing; Dekker-style code needs volatile or a full fence.
