tryLock for deadlock avoidance, reentrancy, limits of synchronized, AtomicInteger vs synchronized, AtomicReference vs volatile, read/write locks and ReentrantReadWriteLock, StampedLock vs ReentrantLock, CAS and lock-free/non-blocking algorithms, busy spinning, LongAdder vs AtomicLong, CountDownLatch vs CyclicBarrier vs Semaphore, Phaser and Exchanger, thread affinity, a thread-safe singleton without synchronization, and refactoring unsafe code with synchronization.
Published September 25, 2026
For each tool, say what problem it solves and what it costs:
Senior answers mention contention, fairness, reentrancy and failure modes (lost signals, starvation, ABA).
ReentrantLock.tryLock() help avoid deadlock?Short answer: A deadlock needs threads that wait forever while holding locks. tryLock() (immediately) or tryLock(timeout, unit) lets a thread give up if it can't get the second lock. It releases what it holds, backs off (with some random jitter), and retries, or fails the operation. That breaks the "hold and wait" condition.
boolean transfer(Account from, Account to, BigDecimal amount) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1);
while (System.nanoTime() < deadline) {
if (from.lock.tryLock()) {
try {
if (to.lock.tryLock()) {
try { from.debit(amount); to.credit(amount); return true; }
finally { to.lock.unlock(); }
}
} finally { from.lock.unlock(); }
}
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 10)); // random back-off avoids livelock
}
return false; // report a timeout instead of hanging
}
Key points to cover:
Learn it in depth → synchronized and Locks
Semaphore and a CountDownLatch? What is a Semaphore? What is a CountDownLatch?Short answer:
Semaphore(n): holds n permits. acquire() takes one, blocking if none are left, and release() returns one. It limits concurrent access to a resource: a connection limit, a bulkhead, rate-limiting parallel calls. It's reusable, and any thread may release a permit (no ownership).CountDownLatch(n): a one-shot gate. Threads await() until the count reaches zero, through countDown() calls from other threads. It can't be reset. Uses: wait for N services to initialise, start N workers simultaneously (a latch of 1 as a "starting gun"), or wait for N tasks to finish.Semaphore maxConcurrentCalls = new Semaphore(20);
<T> T callWithLimit(Callable<T> call) throws Exception {
maxConcurrentCalls.acquire();
try { return call.call(); } finally { maxConcurrentCalls.release(); }
}
Learn it in depth → Concurrent Utilities & Coordination
CyclicBarrier differ from CountDownLatch? When would you use each?Short answer:
CountDownLatch | CyclicBarrier | |
|---|---|---|
| Who waits | Any threads (the waiters and the counters can be different) | The participating threads wait for each other |
| Trigger | countDown() calls reach zero | await() called by all N parties |
| Reusable | No (one-shot) | Yes (it resets after each trip) |
| Extra | — | An optional barrier action runs when the barrier trips. A failure breaks it for everyone |
Phaser when the number of parties changes.synchronized and with ReentrantLock?Short answer: Yes, both are reentrant. A thread that holds the lock can acquire it again: a synchronized method can call another synchronized method on the same object. The lock keeps a hold count, and is only released when the count returns to zero. For ReentrantLock, every lock() needs a matching unlock() (getHoldCount() shows the depth). Reentrancy prevents self-deadlock in call chains, and in overriding methods that call super.
Common trap: StampedLock is not reentrant. Acquiring its write lock twice from the same thread deadlocks (Q9).
synchronized blocks?Short answer: Intrinsic locks are simple and well optimised, but they can't:
tryLock);notifyAll);Other issues:
synchronized pinned virtual threads (fixed in Java 24).On the positive side: it's impossible to forget the unlock, and there's less boilerplate.
AtomicInteger over synchronized?Short answer: For single-variable updates: counters, sequence numbers, flags, compare-and-set state transitions. incrementAndGet(), getAndUpdate() and compareAndSet() are lock-free (hardware CAS), with no blocking or context switches, and they scale well under moderate contention.
Use synchronized/locks when an invariant spans several variables (for example, balance and transactionCount must change together), or for check-then-act logic over collections. For heavily contended counters, prefer LongAdder (Q13).
AtomicReference and a volatile reference?Short answer:
volatile reference gives visibility and ordering: reads see the latest write. But a read-modify-write (for example "replace this if it's still that") isn't atomic, and two threads can overwrite each other.AtomicReference adds atomic compound operations: compareAndSet(expected, new), getAndSet, updateAndGet(fn), accumulateAndGet. That enables lock-free, copy-on-write updates of immutable state.private final AtomicReference<Config> config = new AtomicReference<>(Config.defaults());
void enableFeature(String flag) {
config.updateAndGet(c -> c.withFlag(flag, true)); // CAS retry loop; the function must be side-effect free
}
Use volatile for "publish a new value, and everyone sees it". Use AtomicReference when the new value depends on the old one.
ReentrantReadWriteLock differ from a standard lock?Short answer: A ReentrantReadWriteLock has two locks:
It's a good fit for read-mostly data where reads are non-trivial, and writes are rare.
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Map<String, Rate> rates = new HashMap<>();
Rate get(String ccy) {
rw.readLock().lock();
try { return rates.get(ccy); } finally { rw.readLock().unlock(); }
}
void refresh(Map<String, Rate> fresh) {
rw.writeLock().lock();
try { rates.clear(); rates.putAll(fresh); } finally { rw.writeLock().unlock(); }
}
Differences from a plain ReentrantLock:
Often, an immutable snapshot in a volatile field or ConcurrentHashMap beats read/write locks entirely.
StampedLock and ReentrantLock? What is StampedLock?Short answer: StampedLock (Java 8) offers three modes, each returning a long stamp:
validate(stamp). If a write happened in between, you retry with a real read lock.Optimistic reads make it the fastest option for read-heavy, short reads.
The caveats:
Conditions, and isn't Lock or owner-aware.*Interruptibly variants).ReentrantLock is general-purpose: reentrant, with conditions, fairness, and interruptible acquisition.
double distanceFromOrigin() { // the canonical optimistic-read pattern
long stamp = sl.tryOptimisticRead();
double cx = x, cy = y;
if (!sl.validate(stamp)) { // a write intervened: fall back to a read lock
stamp = sl.readLock();
try { cx = x; cy = y; } finally { sl.unlockRead(stamp); }
}
return Math.hypot(cx, cy);
}
Short answer: An atomic CPU instruction (x86 LOCK CMPXCHG; ARM LL/SC or LSE CAS): "if memory location V still holds the expected value A, set it to B, and report whether that succeeded". Java exposes it through Atomic*.compareAndSet, VarHandle.compareAndSet, and internally Unsafe. Lock-free algorithms use CAS retry loops: read, compute, CAS, and retry on failure.
Key points to cover:
AtomicStampedReference) or garbage-collected nodes.LongAdder spreads the updates.Short answer:
Blocking algorithms use locks. A thread holding a lock can be descheduled, and everyone else waits. They risk deadlock and priority inversion.
Non-blocking algorithms use atomic operations (CAS), so a stalled thread can't stop the others:
They're used in ConcurrentLinkedQueue (Michael-Scott queue), ConcurrentSkipListMap, the Atomic* classes, and parts of ConcurrentHashMap.
Designing one:
Prefer existing JDK structures. Hand-written lock-free code is notoriously hard to get right.
public final class LockFreeStack<T> { // Treiber stack
private record Node<T>(T value, Node<T> next) {}
private final AtomicReference<Node<T>> head = new AtomicReference<>();
public void push(T v) {
Node<T> oldHead, newHead;
do { oldHead = head.get(); newHead = new Node<>(v, oldHead); }
while (!head.compareAndSet(oldHead, newHead));
}
public T pop() {
Node<T> oldHead;
do { oldHead = head.get(); if (oldHead == null) return null; }
while (!head.compareAndSet(oldHead, oldHead.next()));
return oldHead.value();
}
}
Short answer: A thread loops continuously, checking a condition, instead of blocking (while (!ready) { Thread.onSpinWait(); }).
BusySpinWaitStrategy), and briefly inside locks (adaptive spinning before parking).Use it only on dedicated cores, for very short expected waits. Otherwise use park/blocking. The condition variable must be volatile (or atomic), or the loop may never see the update. Thread.onSpinWait() (Java 9) emits CPU pause hints.
LongAdder and AtomicLong?Short answer:
AtomicLong: a single variable updated by CAS. Under heavy contention, many threads fail and retry CAS on the same cache line, and throughput collapses.LongAdder (Java 8): striped counters. There's a base value plus an array of Cells (padded to avoid false sharing). Contended threads update different cells, and sum() adds them up. It gives much higher write throughput under contention, but:
sum() is not an atomic snapshot while updates continue;compareAndSet;Use LongAdder for metrics and hit counters (write-heavy, read occasionally). Use AtomicLong for sequence numbers or IDs, where each caller needs the exact new value. LongAccumulator generalises this to any associative function (max, min).
Short answer: java.util.concurrent.atomic provides lock-free, thread-safe single variables:
AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference;AtomicIntegerArray);AtomicLongFieldUpdater);AtomicStampedReference/AtomicMarkableReference;LongAdder/LongAccumulator/DoubleAdder.Limitations:
AtomicReference.LongAdder).updateAndGet functions may run several times, so they must be side-effect free.VarHandles on plain fields avoid that.Phaser?Short answer: A reusable, multi-phase barrier with a dynamic number of parties (Java 7):
register, arriveAndDeregister);arriveAndAwaitAdvance);arrive), tiering (a tree of phasers, for huge numbers of parties), and a termination hook (onAdvance).It generalises both CyclicBarrier (fixed parties) and CountDownLatch (one-shot). Use it when workers join or leave between phases: for example, a crawler that spawns tasks per level.
Exchanger work?Short answer: A synchronisation point where two threads swap objects. Each calls exchange(myObject), which blocks until the partner arrives, then each receives the other's object. The classic use is a double-buffering pipeline: a producer fills buffer A while the consumer drains buffer B, and at the rendezvous they exchange buffers, with no allocation or copying. It's rarely used in business code. BlockingQueues are more common.
Short answer: Pinning a thread to a specific CPU core (or set of cores), so the OS scheduler doesn't migrate it. That keeps its caches warm, and avoids interference, which gives more predictable latency. It's used in low-latency trading, and with busy-spinning threads, together with isolated cores (isolcpus), NUMA-aware memory placement and IRQ steering. Java has no standard API for it. Use OS tools (taskset, numactl, cgroups cpuset) or libraries (OpenHFT Java-Thread-Affinity, through JNA). In Kubernetes: the static CPU manager policy with guaranteed QoS pods.
Key points to cover:
Short answer: Two standard approaches, both relying on class initialisation guarantees (the JVM initialises a class exactly once, thread-safely, and lazily on first use):
public final class Registry { // initialisation-on-demand holder (Bill Pugh)
private Registry() { }
private static final class Holder { static final Registry INSTANCE = new Registry(); }
public static Registry getInstance() { return Holder.INSTANCE; } // Holder loads on the first call
}
public enum IdGenerator { // enum singleton: serialisation- and reflection-safe
INSTANCE;
private final AtomicLong next = new AtomicLong();
public long nextId() { return next.incrementAndGet(); }
}
The JVM's class-init lock does the synchronisation once. After that, access is plain field reads. An eager static final field is also thread-safe (it's just not lazy).
Key points to cover:
synchronized (once), and requires a volatile field.Short answer:
@GuardedBy("lock")).this or a public object, so outside code can't interfere.public class InventoryBefore { // races: check-then-act on the shared map
private final Map<String, Integer> stock = new HashMap<>();
public boolean reserve(String sku, int qty) {
int available = stock.getOrDefault(sku, 0);
if (available < qty) return false;
stock.put(sku, available - qty);
return true;
}
}
public class InventoryAfter {
private final Object lock = new Object();
@GuardedBy("lock") private final Map<String, Integer> stock = new HashMap<>();
public boolean reserve(String sku, int qty) {
synchronized (lock) { // the whole check-then-act is atomic
int available = stock.getOrDefault(sku, 0);
if (available < qty) return false;
stock.put(sku, available - qty);
return true;
}
}
}
// Or, without an explicit lock: stock.computeIfPresent(sku, (k, v) -> v >= qty ? v - qty : v) on a ConcurrentHashMap
Q: What is a fair lock, and why isn't it the default?
A: A fair lock (new ReentrantLock(true)) grants access in FIFO arrival order, which prevents starvation. It's much slower under contention: no barging, and more context switches. Non-fair locks let a thread that's already running grab a free lock immediately, which gives better throughput.
Q: What is a Condition, and why is it better than wait/notify?
A: lock.newCondition() gives separate wait sets per condition (for example notFull/notEmpty in a bounded buffer). You can signal exactly the right waiters, with timed and interruptible awaits, all tied to an explicit Lock.
Q: What is the ABA problem, concretely? A: Thread 1 reads the head A of a lock-free stack, and is delayed. Thread 2 pops A and B, then pushes A back. Thread 1's CAS(A → A.next) succeeds, but A.next is now stale (B was removed), which corrupts the stack. Versioned references, or never reusing nodes (GC-safe allocation), prevent it.
Q: How would you rate-limit calls with a Semaphore, compared with a token bucket?
A: A Semaphore limits concurrency (in-flight calls), not rate (calls per second). For rate, use a token bucket (Guava's RateLimiter, Bucket4j, or Resilience4j's RateLimiter) that refills permits over time.