Concurrency Deep Dive
Lock Interfaces
Section titled “Lock Interfaces”ReentrantLock
Section titled “ReentrantLock”java.util.concurrent.locks.ReentrantLock provides mutual exclusion with features beyond synchronized: fair/unfair ordering, timed lock acquisition, interruptible lock acquisition, and Multiple condition variables per lock.
import java.util.concurrent.locks.ReentrantLock;
public class Counter { private int count = 0; private final ReentrantLock lock = new ReentrantLock();
public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } }
public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }}Fair vs Non-fair locking:
- Non-fair (default): Threads can barge — a newly arriving thread may acquire the lock before a waiting thread. Higher throughput because threads do not need to be woken up and immediately put back to sleep.
- Fair: Threads acquire the lock in the order they requested it. Lower throughput due to context-switch overhead, but avoids starvation.
ReentrantLock fairLock = new ReentrantLock(true); // fairReentrantLock unfairLock = new ReentrantLock(false); // non-fair (default)Reentrancy: A thread that holds the lock can acquire it again without deadlocking. The lock Keeps a hold count; unlock decrements the count, and the lock is released only when the count Reaches zero.
ReentrantLock lock = new ReentrantLock();lock.lock(); // hold count = 1lock.lock(); // hold count = 2 (same thread)lock.unlock(); // hold count = 1lock.unlock(); // hold count = 0, lock releasedTimed and interruptible lock acquisition:
ReentrantLock lock = new ReentrantLock();
try { if (lock.tryLock(5, TimeUnit.SECONDS)) { try { // critical section } finally { lock.unlock(); } } else { // failed to acquire lock within timeout }} catch (InterruptedException e) { Thread.currentThread().interrupt();}ReentrantReadWriteLock
Section titled “ReentrantReadWriteLock”A read-write lock allows multiple concurrent readers but only one exclusive writer. When a writer Holds the lock, no readers or other writers can acquire it. This is useful for read-mostly data Structures (caches, configuration).
public class ThreadSafeCache<K, V> { private final Map<K, V> cache = new HashMap<>(); private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock();
public V get(K key) { readLock.lock(); try { return cache.get(key); } finally { readLock.unlock(); } }
public void put(K key, V value) { writeLock.lock(); try { cache.put(key, value); } finally { writeLock.unlock(); } }
public V computeIfAbsent(K key, Function<K, V> loader) { V value = get(key); if (value != null) return value;
writeLock.lock(); try { // Double-check after acquiring write lock value = cache.get(key); if (value == null) { value = loader.apply(key); cache.put(key, value); } return value; } finally { writeLock.unlock(); } }}Downgrade from write to read: A thread holding the write lock can acquire the read lock without Releasing the write lock first. Then it can release the write lock, effectively downgrading. Upgrade from read to write is NOT supported — attempting to acquire the write lock while holding The read lock causes deadlock.
StampedLock
Section titled “StampedLock”Introduced in JDK 8, StampedLock provides optimistic reads that do not block writers. It is Designed for read-mostly scenarios where ReentrantReadWriteLock is too coarse.
public class Point { private double x, y; private final StampedLock sl = new StampedLock();
// Write — exclusive void move(double deltaX, double deltaY) { long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } }
// Optimistic read — NO blocking of writers double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); double currentX = x, currentY = y; if (!sl.validate(stamp)) { // A write occurred — fall back to read lock stamp = sl.readLock(); try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); }
// Read — blocking double[] getPosition() { long stamp = sl.readLock(); try { return new double[]{x, y}; } finally { sl.unlockRead(stamp); } }}StampedLock is not reentrant. Each call to writeLock``readLockOr tryOptimisticRead returns A long stamp that must be used to unlock. This is a deliberate design choice — the lack of Reentrancy prevents certain deadlock patterns and allows the optimistic read mechanism.
Condition (from java.util.concurrent.locks) provides await/signal semantics similar to Object.wait/Object.notify but with more features: multiple conditions per lock, interruptible Waits, timed waits, and fairness.
Bounded Buffer Pattern
Section titled “Bounded Buffer Pattern”public class BoundedBuffer<T> { private final Queue<T> queue = new LinkedList<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // releases lock, waits for signal } queue.add(item); notEmpty.signal(); // wake one waiting consumer } finally { lock.unlock(); } }
public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } T item = queue.poll(); notFull.signal(); // wake one waiting producer return item; } finally { lock.unlock(); } }}The fairness of Condition follows the fairness of its associated ReentrantLock. A fair lock”s Condition queues are FIFO; a non-fair lock’s condition queues may allow barging.
Atomic Operations
Section titled “Atomic Operations”AtomicInteger``AtomicLong``AtomicReference
Section titled “AtomicInteger``AtomicLong``AtomicReference”The atomic classes use CPU-level compare-and-swap (CAS) instructions to provide lock-free, Thread-safe operations on single variables.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomically increment, return new valuecounter.getAndIncrement(); // atomically increment, return old valuecounter.addAndGet(5); // atomically add 5, return new valuecounter.compareAndSet(10, 20); // if current == 10, set to 20; return true/false
// Lock-free maxAtomicInteger max = new AtomicInteger(0);int current;do { current = max.get();} while (!max.compareAndSet(current, Math.max(current, newValue)));getAndUpdate and accumulateAndGet
Section titled “getAndUpdate and accumulateAndGet”AtomicInteger counter = new AtomicInteger(0);
// getAndUpdate — applies function, returns previous valueint prev = counter.getAndUpdate(n -> n * 2);
// updateAndGet — applies function, returns new valueint next = counter.updateAndGet(n -> n + 1);
// accumulateAndGet — int-specific, avoids boxingint sum = counter.accumulateAndGet(5, Integer::sum);// Equivalent to: counter = counter + 5
// getAndAccumulateint old = counter.getAndAccumulate(5, Integer::sum);AtomicStampedReference
Section titled “AtomicStampedReference”AtomicReference cannot detect the ABA problem: a thread reads value A, another thread changes it To B then back to A, and the first thread’s CAS succeeds despite the value having been changed in Between. AtomicStampedReference adds a version stamp to detect this.
AtomicStampedReference<String> ref = new AtomicStampedReference<>("initial", 0);
int[] stampHolder = new int[1];String value = ref.get(stampHolder); // value = "initial", stampHolder[0] = 0int stamp = stampHolder[0];
boolean success = ref.compareAndSet("initial", "updated", stamp, stamp + 1);// CAS checks both value AND stampLongAdder and LongAccumulator
Section titled “LongAdder and LongAccumulator”For high-contention counters where AtomicLong becomes a bottleneck due to CAS contention, LongAdder distributes increments across an array of cells and sums them on read. Write contention Is eliminated; read requires summing all cells.
// AtomicLong — single variable, CAS contention under high contentionAtomicLong counter = new AtomicLong();
// LongAdder — distributed cells, better under high contentionLongAdder adder = new LongAdder();adder.increment();adder.add(5L);long total = adder.sum();
// LongAccumulator — general-purpose with custom functionLongAccumulator max = new LongAccumulator(Long::max, Long.MIN_VALUE);max.accumulate(42L);max.accumulate(17L);long result = max.get(); // 42The volatile Keyword
Section titled “The volatile Keyword”Visibility Guarantees
Section titled “Visibility Guarantees”A volatile field has two guarantees:
- Visibility: A write to a
volatilefield is immediately visible to all other threads. The JIT and CPU cannot cache the value in a register or reorder reads/writes past the volatile access. - Ordering: Reads and writes of
volatilefields establish happens-before relationships. No reads or writes of volatile variables can be reordered with respect to each other.
public class VolatileFlag { private volatile boolean shutdownRequested = false;
public void shutdown() { shutdownRequested = true; // visible to all threads immediately }
public void doWork() { while (!shutdownRequested) { // Without volatile, the JIT might optimize this loop to: // if (!shutdownRequested) { while (true) { ... } } // because it can cache shutdownRequested in a register } }}When to Use volatile
Section titled “When to Use volatile”Use volatile for:
- Flags and status indicators — one thread writes, others read (shutdown flags, initialization flags).
- One-shot publication — writing a reference to a fully constructed object exactly once.
- Read-heavy counters where approximate accuracy is acceptable (use
AtomicIntegerinstead if exactness is required).
Do NOT use volatile for:
- Compound operations like
count++(read-modify-write is not atomic — useAtomicInteger). - Multi-variable invariants where consistency between variables matters (use a lock).
// BUG — volatile does not make count++ atomicpublic class VolatileCounter { private volatile int count = 0; // NOT thread-safe for increment
public void increment() { count++; // read, add, write — NOT atomic }}
// FIX — use AtomicIntegerpublic class SafeCounter { private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }}Concurrent Collections
Section titled “Concurrent Collections”ConcurrentHashMap
Section titled “ConcurrentHashMap”The workhorse concurrent map. JDK 8+ uses a lock-free CAS-based approach for most operations, with Synchronized blocks only on individual buckets during resize.
ConcurrentHashMap<String, Long> wordCounts = new ConcurrentHashMap<>();
// Atomic operations — no external synchronization neededwordCounts.put("hello", 1L);wordCounts.merge("hello", 1L, Long::sum); // atomically adds 1Long count = wordCounts.getOrDefault("hello", 0L);
// Atomic compute — the mapping function runs atomicallywordCounts.computeIfAbsent("world", k -> { // expensive computation — runs at most once per key return loadFromDatabase(k);});
// forEach with parallelismwordCounts.forEach(4, (key, value) -> { System.out.println(key + ": " + value);});
// searchString firstKey = wordCounts.search(4, (key, value) -> value > 1000 ? key : null);
// reducelong total = wordCounts.reduceValuesToLong(4, Long::longValue, 0, Long::sum);An unbounded, thread-safe, FIFO queue based on a linked list. Uses lock-free CAS for all operations. ConcurrentLinkedDeque is the double-ended variant.
ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>();
queue.offer(new Task("process"));queue.offer(new Task("analyze"));
Task task = queue.poll(); // null if emptyTask peek = queue.peek(); // null if emptyCopyOnWriteArrayList
Section titled “CopyOnWriteArrayList”Every write operation (add, set, remove) creates a fresh copy of the underlying array. Reads proceed Without locking against the current array snapshot. This makes reads extremely fast and writes Expensive. Ideal for read-heavy workloads with infrequent writes (listener lists, configuration).
CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();
// Fast read — no synchronizationfor (EventListener listener : listeners) { listener.onEvent(event); // safe even during concurrent modification}
// Expensive write — copies entire arraylisteners.add(newListener);Blocking queues are designed for producer-consumer patterns. put blocks when full, take blocks When empty.
| Implementation | Bounded | Ordering | Notes |
|---|---|---|---|
ArrayBlockingQueue | Yes (fixed) | FIFO | Backed by array |
LinkedBlockingQueue | Optional | FIFO | Backed by linked nodes |
PriorityBlockingQueue | No (unbounded) | Priority (natural or Comparator) | Never blocks on put |
SynchronousQueue | Zero capacity | None | Handoff — put waits for take |
DelayQueue | Unbounded | By delay time | Elements implement Delayed |
// Producer-consumer with LinkedBlockingQueueBlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
// ProducerExecutorService producers = Executors.newFixedThreadPool(4);producers.submit(() -> { while (!Thread.currentThread().isInterrupted()) { Task task = generateTask(); queue.put(task); // blocks if queue is full }});
// ConsumerExecutorService consumers = Executors.newFixedThreadPool(4);consumers.submit(() -> { while (!Thread.currentThread().isInterrupted()) { Task task = queue.take(); // blocks if queue is empty process(task); }});SynchronousQueue has zero capacity — each put must wait for a matching take and vice Versa. It is used by Executors.newCachedThreadPool() to hand off tasks directly to worker threads Without buffering.
ForkJoin Framework
Section titled “ForkJoin Framework”The ForkJoin framework (JDK 7) is designed for divide-and-conquer algorithms that can be Parallelized by splitting work into smaller subtasks. It uses a work-stealing scheduler: idle Threads steal subtasks from busy threads’ queues.
RecursiveTask (returns a result)
Section titled “RecursiveTask (returns a result)”public class ParallelMergeSort extends RecursiveTask<long[]> { private final long[] array; private final int lo, hi;
public ParallelMergeSort(long[] array, int lo, int hi) { this.array = array; this.lo = lo; this.hi = hi; }
@Override protected long[] compute() { if (hi - lo < 8192) { // Sequential threshold — sort small arrays sequentially Arrays.sort(array, lo, hi); return array; }
int mid = (lo + hi) / 2; ParallelMergeSort left = new ParallelMergeSort(array, lo, mid); ParallelMergeSort right = new ParallelMergeSort(array, mid, hi);
left.fork(); // async execution in the pool long[] rightResult = right.compute(); // execute in current thread long[] leftResult = left.join(); // wait for forked task
return merge(leftResult, rightResult, lo, mid, hi); }
private long[] merge(long[] left, long[] right, int lo, int mid, int hi) { long[] sorted = new long[hi - lo]; int i = 0, j = mid - lo, k = 0; while (i < mid - lo && j < hi - lo) { sorted[k++] = left[i] <= right[j] ? left[i++] : right[j++]; } while (i < mid - lo) sorted[k++] = left[i++]; while (j < hi - lo) sorted[k++] = right[j++]; System.arraycopy(sorted, 0, array, lo, sorted.length); return array; }}RecursiveAction (no result)
Section titled “RecursiveAction (no result)”public class ParallelArrayFill extends RecursiveAction { private final double[] array; private final int lo, hi; private final double value;
public ParallelArrayFill(double[] array, int lo, int hi, double value) { this.array = array; this.lo = lo; this.hi = hi; this.value = value; }
@Override protected void compute() { if (hi - lo < 10000) { Arrays.fill(array, lo, hi, value); } else { int mid = (lo + hi) / 2; invokeAll(new ParallelArrayFill(array, lo, mid, value), new ParallelArrayFill(array, mid, hi, value)); } }}ForkJoinPool
Section titled “ForkJoinPool”ForkJoinPool pool = new ForkJoinPool(); // uses Runtime.availableProcessors() workersForkJoinPool customPool = new ForkJoinPool(16); // 16 worker threads
long[] data = new long[10_000_000];// Fill with random data...
long[] sorted = pool.invoke(new ParallelMergeSort(data, 0, data.length));Choosing the sequential threshold: Too low and the overhead of task creation dominates. Too high And you lose parallelism. A good starting point is an array size that takes 10-100 microseconds to Process sequentially. Profile and adjust.
ThreadLocal
Section titled “ThreadLocal”ThreadLocal<T> provides thread-confined variables — each thread has its own independently Initialized copy. No synchronization is needed because threads never share the value.
public class UserIdContext { private static final ThreadLocal<String> CURRENT_USER = ThreadLocal.withInitial(() -> "anonymous");
public static void set(String userId) { CURRENT_USER.set(userId); }
public static String get() { return CURRENT_USER.get(); }
public static void clear() { CURRENT_USER.remove(); // CRITICAL — prevents memory leaks }}Memory Leak Pitfalls
Section titled “Memory Leak Pitfalls”ThreadLocal values are stored in each thread’s ThreadLocalMap. If a thread is long-lived (e.g., A web server worker thread in a thread pool), and you do not call remove()The value will remain In memory for the lifetime of the thread. This is the most common ThreadLocal leak.
// DANGEROUS — in a servlet container with pooled threadspublic void doGet(HttpServletRequest req, HttpServletResponse resp) { ThreadLocal<Connection> connectionHolder = ...; // instance variable connectionHolder.set(dataSource.getConnection()); try { // process request } finally { // BUG — connection not closed, not removed from ThreadLocal // The Connection leaks because the thread is returned to the pool }}
// SAFE — always clean up in finallypublic void doGet(HttpServletRequest req, HttpServletResponse resp) { connectionHolder.set(dataSource.getConnection()); try { processRequest(); } finally { connectionHolder.get().close(); connectionHolder.remove(); // prevent leak }}Prevention with Lock Ordering
Section titled “Prevention with Lock Ordering”Deadlock occurs when thread A holds lock X and waits for lock Y, while thread B holds lock Y and Waits for lock X. The simplest prevention strategy is to always acquire locks in a global, Consistent order.
// DEADLOCK-PRONEvoid transfer(Account from, Account to, int amount) { synchronized (from) { // lock A synchronized (to) { // lock B — potential deadlock from.debit(amount); to.credit(amount); } }}
// SAFE — consistent lock orderingvoid safeTransfer(Account from, Account to, int amount) { Account first = from.getId() < to.getId() ? from : to; Account second = from.getId() < to.getId() ? to : from; synchronized (first) { synchronized (second) { from.debit(amount); to.credit(amount); } }}Detection with jconsole
Section titled “Detection with jconsole”Run your application with -Djdk.attach.allowAttachSelf=true and attach jconsole. The “Threads” Tab shows deadlocked threads. Programmatically, use ThreadMXBean:
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = mxBean.findDeadlockedThreads();if (deadlockedThreads != null) { ThreadInfo[] infos = mxBean.getThreadInfo(deadlockedThreads); for (ThreadInfo info : infos) { System.err.println("Deadlocked thread: " + info.getThreadName()); System.err.println(" Waiting on: " + info.getLockName()); for (StackTraceElement ste : info.getStackTrace()) { System.err.println(" at " + ste); } }}Virtual Threads Integration
Section titled “Virtual Threads Integration”Project Loom (JDK 21) introduces virtual threads — lightweight threads managed by the JVM, not the OS. Virtual threads make blocking operations cheap, reducing the need for complex asynchronous Programming.
// Traditional platform threads — expensive, limited by OSExecutorService platformPool = Executors.newFixedThreadPool(200);
// Virtual threads — millions of concurrent tasksExecutorService virtualPool = Executors.newVirtualThreadPerTaskExecutor();
List<Future<String>> futures = new ArrayList<>();for (int i = 0; i < 10_000; i++) { futures.add(virtualPool.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); // blocks the virtual thread, NOT the carrier return "result-" + Thread.currentThread().threadId(); }));}Virtual Threads and synchronized
Section titled “Virtual Threads and synchronized”virtualPool.close(); // waits for all tasks
Virtual threads can use synchronizedBut it pins the carrier thread (the underlying platform Thread) for the duration of the synchronized block. If many virtual threads are pinned Simultaneously, the carrier thread pool can be exhausted. Use ReentrantLock instead of synchronized in code that will run on virtual threads.
// PREFERRED for virtual threadsprivate final ReentrantLock lock = new ReentrantLock();
public void doWork() { lock.lock(); try { // critical section } finally { lock.unlock(); }}
// Avoid — pins carrier threadpublic synchronized void doWorkSync() { // critical section}Intuition
Section titled “Intuition”Deep dive into parallelism: Advanced concurrency is like managing a team of workers — locks, semaphores, and executors are tools for coordinating parallel tasks efficiently.
Why it matters: Deep understanding of concurrency helps you build high-performance, thread-safe applications that scale to millions of users.
The key insight: Lock-free algorithms can be faster than traditional synchronization — they avoid the overhead of acquiring and releasing locks.
Common Pitfalls
Section titled “Common Pitfalls”Forgetting finally { lock.unlock(); }
Section titled “Forgetting finally { lock.unlock(); }”// BUG — lock is never released if an exception occurspublic void process() { lock.lock(); doRiskyOperation(); // if this throws, lock is never released lock.unlock();}
// CORRECTpublic void process() { lock.lock(); try { doRiskyOperation(); } finally { lock.unlock(); }}ConcurrentModificationException on Concurrent Collections
Section titled “ConcurrentModificationException on Concurrent Collections”// Even concurrent collections can throw CME during bulk operationsConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();for (String key : map.keySet()) { // This is safe for ConcurrentHashMap — no CME map.remove(key); // ConcurrentHashMap's iterator is weakly consistent}
// But iterating with Stream operations that modify the map can cause issuesmap.forEach((key, value) -> { map.put(key + "2", value); // may cause CME during iteration});Using synchronized on Long or Integer Wrappers
Section titled “Using synchronized on Long or Integer Wrappers”// BUG — synchronized on auto-boxed Integer// Each boxing creates a NEW Integer object, so the lock is on different objectsprivate Integer counter = 0;
public void increment() { synchronized (counter) { // BUG — counter is re-boxed each time counter++; }}
// FIX — use a dedicated lock objectprivate final Object lock = new Object();private int counter = 0;
public void increment() { synchronized (lock) { counter++; }}AtomicReference ABA Problem
Section titled “AtomicReference ABA Problem”// BUG — ABA problem with AtomicReferenceAtomicReference<Node> head = new AtomicReference<>(nodeA);// Thread 1: reads head = nodeA, gets preempted// Thread 2: sets head = nodeB// Thread 3: sets head = nodeA (same reference, different state)// Thread 1: CAS succeeds, but the list structure has changed!head.compareAndSet(nodeA, newNode); // succeeds incorrectly
// FIX — use AtomicStampedReferenceAtomicStampedReference<Node> stampedHead = new AtomicStampedReference<>(nodeA, 1);Thread Pool Starvation
Section titled “Thread Pool Starvation”// DEADLOCK — all threads in the pool are waiting for tasks from the same poolExecutorService pool = Executors.newFixedThreadPool(10);pool.submit(() -> { Future<String> f = pool.submit(() -> "result"); // waits for another thread return f.get(); // but all 10 threads may be doing this});This topic covers the core concepts of concurrency deep dive, including underlying theory, practical implementation, and key applications.
Key concepts include:
- core concepts and terminology
- algorithms and computational thinking
- practical implementation
- security and ethical considerations
- applications in the real world
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Types and Variables: Thread-safe variable access using volatile and final.
- Garbage Collection: GC interaction with concurrent collections and memory visibility.
- Generics: Generic concurrent data structures and type-safe synchronization.