Java Util Concurrency Utilities Overview
The Java Util Concurrency package delivers a comprehensive set of utilities for concurrent programming, including atomic classes, synchronization primitives, and coordination mechanisms. This package enables developers to build thread-safe applications with greater ease and efficiency compared to traditional synchronized blocks.
Atomic Classes and CAS Mechanism
The atomic classes in Java rely on Compare-And-Swap (CAS) operations, which are implemented through the Unsafe class at the native level. CAS represents a CPU-level atomic instruction that works as follows: the operation takes both an expected old value and a desired new value, then updates the value only if the current value matches the expected old value. Because CAS operations are atomic, they eliminate the need for locks in concurrent scenarios, making this an optimistic locking approach.
Three Major Limitations of CAS
- ABA Problem: CAS checks only the current value against the expected value, but cannot detect intermediate changes. For instance, if a value changes from A to B and back to A, CAS would consider it unchanged. The solution involves adding version numbers, incrementing with each update.
- Long Spin Loop Overhead: In spin-wait scenarios, the CPU continuously executes instructions, consuming processing resources.
- Single Variable Atomicity: CAS can only guarantee atomicity for a single shared variable. When operating on multiple shared variables, CAS loops fail to maintain atomic operations—locks become necessary in such cases.
Spin Lock vs Adaptive Spin Lock
The motivation for spin locks stems from the significant overhead of thread context switching. When a thread acquires or releases a lock, the operating system must save and restore the thread's execution context, which involves CPU state transitions. For very short-duration locks, this context-switching cost exceeds the actual lock-holding time.
Spin locks address this by allowing threads to busy-wait rather than enter a blocked state. The acquiring thread continues spinning on the CPU, waiting for the lock to become available, then immediately proceeds once released. However, spin duration must be bounded to prevent wasteful CPU consumption—most JVM implementations cap spin iterations at approximately 10 attempts before resorting to blocking.
Adaptive Spin Lock introduces intelligence by adjusting spin duration based on historical lock behavior. If the previous lock acquisition succeeded quickly, the spin duration encreases. Conversely, if attempts frequently fail, the spin count decreases, assuming the lock is more contested.
The Unsafe Class
The Unsafe class resides in sun.misc and provides low-level operations for direct memory access and unsafe operations. This class serves internal JDK purposes and remains a non-public API. Direct usage in production code is strongly discouraged due to potential undefined behavior and lack of cross-platform compatibility guarantees.
LockSupport Implementation
LockSupport serves as the foundational mechanism for JUC synchronization primitives. Understanding its internals is essential for comprehending higher-level concurrency utilities.
public class LockSupport {
// Blocks the current thread with a blocker object for diagnostics
public static void park(Object blocker) {
Thread currentThread = Thread.currentThread();
setBlocker(currentThread, blocker);
UNSAFE.park(false, 0L);
setBlocker(currentThread, null);
}
// Unblocks the specified thread
public static void unpark(Thread thread) {
if (thread != null) {
UNSAFE.unpark(thread);
}
}
// Blocks with absolute timeout
public static void parkNanos(Object blocker, long nanoseconds) {
if (nanoseconds > 0) {
Thread currentThread = Thread.currentThread();
setBlocker(currentThread, blocker);
UNSAFE.park(false, nanoseconds);
setBlocker(currentThread, null);
}
}
// Blocks until absolute deadline
public static void parkUntil(Object blocker, long deadline) {
Thread currentThread = Thread.currentThread();
setBlocker(currentThread, blocker);
UNSAFE.park(true, deadline);
setBlocker(currentThread, null);
}
// Retrieves the blocker object for diagnostic purposes
public static Object getBlocker(Thread thread) {
if (thread == null) {
throw new NullPointerException();
}
return UNSAFE.getObjectVolatile(thread, parkBlockerOffset);
}
}
The park() and unpark() methods manage thread blocking and unblocking through a permit mechanism. The permit functions as a binary semaphore with values 0 and 1:
- park(): Blocks the thread if permit equals 0; consumes one permit if greater than 0
- unpark(): Sets permit to 1; permits do not accumulate beyond 1
When permit exceeds 0, park() returns immediately without blocking. When permit equals 0, the thread blocks.
park/unpark vs wait/notify
The LockSupport approach differs fundamentally from the Object wait/notify mechanism:
- Ordering Constraints: wait() must precede notify() in execution order. park/unpark impose no such ordering requirements—unpark() can be called before park().
- Synchronization Context: wait/notify must execute within synchronized blocks. park/unpark work from any execution context since they operate on permit semaphores rather than object monitors.
- Thread Targeting: park/unpark unblock a specific thread. notify() arbitrarily selects one waiting thread, while notifyAll() wakes all waiting threads.
AbstractQueuedSynchronizer Architecture
AQS forms the backbone of most JUC synchronization components. Three fundamental concepts underpin its design:
- State: Manages the status of shared resources through an integer counter
- Queue: Organizes waiting threads in a FIFO structure
- CAS: Ensures atomic state modifications without locks
The core principle: when the requested resource is available, the acquiring thread becomes the owner and the resource enters a locked state. When occupied, threads join a CLH (Craig, Landin, Hagersten) queue to wait their turn. This queue-based approach ensures fair, ordered lock acquisition.
CLH Queue Structure: A FIFO linked queue where each waiting thread holds a node containing its predecessor reference and wait status. When resources release, the head node's successor immediately acquires the resource. Consider it similar to a restaurant queue system with numbered tickets—each person waits their turn based on arrival order, eliminating competition.
AQS State Management:
public abstract class AbstractQueuedSynchronizer {
// Represents lock state: 0 indicates available, positive values indicate held
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
// Atomic state update using CAS
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
AQS operates in two modes: exclusive (one thread holds the lock) and shared (multiple threads can acquire simultaneously). The exclusiveOwnerThread field tracks the current lock holder, though no public accessor exists.
The framework employs the Template Method pattern, defining algorithm skeletons in the abstract base class while deferring specific implementations to concrete subclasses.
Required Implementations for Custom Synchronizers:
boolean isHeldExclusively(); // Check ownership for conditions
boolean tryAcquire(int permits); // Exclusive acquisition attempt
boolean tryRelease(int permits); // Exclusive release attempt
int tryAcquireShared(int permits); // Shared acquisition: negative=fail, zero=success, positive=success+surplus
boolean tryReleaseShared(int permits); // Shared release attempt
All other AQS methods are final and cannot be overridden.
ReentrantLock Behavior: State initializes to 0. When Thread A acquires the lock, tryAcquire() increments state. Other threads fail tryAcquire() until A calls unlock() to decrement state back to 0. Because reentrant locks allow the same thread to acquire multiple times, state increments with each nested acquisition, requiring equal unlock() calls to restore state to zero.
CLH Queue Node Implementation
Each waiting thread becomes a Node in the CLH queue, forming a virtual bidirectional linked list:
class Node {
// Sentinel marker for shared mode
Node() {
}
// Constructor for waiting nodes
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Constructor for condition queue
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
volatile int waitStatus;
volatile Node prev;
volatile Node next;
Node nextWaiter;
Thread thread;
}
Wait Status Values:
Lock Acquisition Flow
The acquire() method implements exclusive lock acquisition:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
The addWaiter() method creates a node and enqueues it:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node predecessor = tail;
if (predecessor != null) {
node.prev = predecessor;
if (compareAndSetTail(predecessor, node)) {
predecessor.next = node;
return node;
}
}
enq(node);
return node;
}
The acquireQueued() method manages spinning until lock acquisition:
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node predecessor = node.predecessor();
if (predecessor == head && tryAcquire(arg)) {
setHead(node);
predecessor.next = null;
return interrupted;
}
if (shouldParkAfterFailedAcquire(predecessor, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} catch (RuntimeException e) {
cancelAcquire(node);
throw e;
}
}
The shouldParkAfterFailedAcquire() method determines whether blocking is appropriate:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int status = pred.waitStatus;
if (status == Node.SIGNAL) {
return true;
}
if (status > 0) {
// Skip cancelled predecessors
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// Set predecessor to SIGNAL for next iteration
compareAndSetWaitStatus(pred, status, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
Key AQS Principles
Four fundamental observations about the sync queue operation:
- Each node receives wakeup signals exclusively from its predecessor
- A node proceeds only when it detects its predecessor is the head AND successfully acquires the lock
- Nodes transition from condition queues to sync queues through signal operasions
- A SIGNAL status indicates that successors are eligible to run