Understanding Java's AbstractQueuedSynchronizer: Locks, Queues, and State Management
This article explains Java's AbstractQueuedSynchronizer (AQS) framework, covering its FIFO queue mechanism, lock acquisition and release processes for exclusive and shared modes, state handling, and the abstract methods developers must implement to build custom synchronizers.
AbstractQueuedSynchronizer (AQS) Overview
AQS provides a FIFO‑based framework for implementing synchronizers such as exclusive locks, shared locks, semaphores, and events. It separates two responsibilities: (1) manipulating the state field that represents the lock’s logical state, and (2) managing a wait queue and the blocking/unblocking of threads. Subclasses implement the abstract methods that operate on state; AQS supplies the queue handling logic.
AQS itself does not implement any public synchronization interface; it only offers low‑level methods like acquireInterruptibly that concrete synchronizers invoke.
MESA Monitor Model in Java
Java follows the MESA monitor model, which guarantees that accesses to shared fields and methods are thread‑safe. In AQS the model is simplified to a single condition variable and a single FIFO wait queue. The queue is used both for exclusive acquisition and, via ConditionObject, for condition‑await semantics.
Acquiring an Exclusive Lock
Basic acquisition (ignoring interrupts)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} tryAcquireis abstract; a subclass defines the actual lock‑state transition. If acquisition fails, the thread is enqueued with addWaiter and then spins in acquireQueued until it becomes the head and can obtain the lock.
Queue node states
CANCELLED (1) : node was cancelled because of timeout or interruption.
SIGNAL (-1) : predecessor will signal this node when it releases.
CONDITION (-2) : used by ConditionObject for condition‑await.
PROPAGATE (-3) : shared‑mode propagation flag.
0 : intermediate state after predecessor has signaled.
Interrupt‑responsive acquisition
acquireInterruptibly(int arg)checks the thread’s interrupt status before attempting to acquire and during the queue spin. If an interrupt is observed, it throws InterruptedException and aborts.
Acquisition with timeout
tryAcquireNanos(int arg, long nanosTimeout)adds a timeout check. Each failed spin decrements the remaining time; before parking the thread it compares the remaining timeout with a spin‑threshold (≈1 ns). Parking is performed via LockSupport.parkNanos.
Releasing an Exclusive Lock
Release consists of two steps:
Invoke the abstract tryRelease(int arg) implemented by the subclass to modify state.
Wake the successor node using unparkSuccessor. The method requires a non‑null head and a non‑zero waitStatus. If the immediate successor’s waitStatus ≤ 0, it is unparked directly; otherwise the queue is scanned backwards to find the nearest viable node.
Acquiring a Shared Lock
Basic shared acquisition (ignoring interrupts)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
} tryAcquireSharedreturns an int indicating the result:
Negative – acquisition failed; the thread must enqueue.
Zero – acquisition succeeded but no further shared acquires will succeed.
Positive – acquisition succeeded and additional shared acquires may also succeed; wake‑up propagation is required.
If the call returns a negative value, the thread is added to the queue (nodes marked with nextWaiter == SHARED) and spins in doAcquireShared until it can acquire.
Interrupt‑responsive shared acquisition
acquireSharedInterruptibly(int arg)checks interrupt status before acquisition and during the spin, throwing InterruptedException when an interrupt is observed.
Shared acquisition with timeout
tryAcquireSharedNanos(int arg, long nanosTimeout)mirrors the exclusive timeout logic: each failed attempt checks the remaining time, and before parking the thread it compares the timeout with the spin‑threshold. Parking uses LockSupport.parkNanos.
Releasing a Shared Lock
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}After tryReleaseShared succeeds, doReleaseShared performs a spin that propagates wake‑ups through the queue. Propagation continues while the head’s waitStatus is –1, 0, or –3 and at least two nodes remain in the queue.
Abstract Methods Required by Subclasses
tryAcquire(int arg)– exclusive lock acquisition. tryRelease(int arg) – exclusive lock release. tryAcquireShared(int arg) – shared lock acquisition. tryReleaseShared(int arg) – shared lock release.
Subclasses may also override isHeldExclusively() to indicate exclusive ownership.
Key Takeaways
AQS implements a reusable FIFO queue template that underlies most Java synchronizers. The waitStatus field of each node encodes whether the thread should wait, be signaled, or propagate wake‑ups, enabling the construction of exclusive locks, shared locks, semaphores, and condition objects with consistent blocking semantics.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
JavaEdge
First‑line development experience at multiple leading tech firms; now a software architect at a Shanghai state‑owned enterprise and founder of Programming Yanxuan. Nearly 300k followers online; expertise in distributed system design, AIGC application development, and quantitative finance investing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
