NOTE
6.26 Fair Lock
A fair lock follows the first-come, first-served principle. Even after the lock has been released, a later-arriving thread cannot barge in; it must wait until nobody is ahead of it. 1. Usage. 2. Principle analysis.
This is a historical learning note and may contain outdated or incomplete understanding.
A fair lock follows the first-come, first-served principle.
Even if the lock has already been released, a thread that arrived later cannot compete for it until nobody is waiting ahead of it.
1. How to Use It
public class TestReentrantLock
{
private static int val = 0;
private final static Lock lock = new ReentrantLock(true);// Fair lock
public static void main(String[] args) throws InterruptedException
{
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
try
{
lock.lock();
val++;
}
finally
{
lock.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++)
{
try
{
lock.lock();
val--;
}
finally
{
lock.unlock();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(val);
}
}
2. Principle Analysis
2.1. Constructor
2.1.1. Implemented with AQS Underneath
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
// Nonfair lock by default
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// If true, a fair lock uses FairSync; otherwise NonfairSync
sync = fair ? new FairSync() : new NonfairSync();
}
// Sync is a subclass of AQS
abstract static class Sync extends AbstractQueuedSynchronizer {}
// FairSync is a subclass of Sync
static final class FairSync extends Sync {}
}
2.2. Locking
lock
public void lock() {
// Call FairSync.lock
sync.lock();
}
2.2.1. Call the Fair Lock’s lock Method
FairSync.lock
final void lock() {
// Call AQS.acquire
acquire(1);
}
2.2.2. Call AQS acquire to Acquire the Lock
AQS.acquire
public final void acquire(int arg) {
// Call FairSync.tryAcquire to acquire the lock
if (!tryAcquire(arg) &&
// If acquisition fails, join the AQS queue and repeatedly block the current thread,
// waiting to be woken to continue acquiring the lock
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// Restore the interrupt flag
selfInterrupt();
}
Because FairSync overrides AQS’s tryAcquire, this calls FairSync.tryAcquire.
The rest of the logic is the same as 5.AQS.md. Only the main logic is briefly described below.
2.2.3. Try to Acquire the Lock [Only the Queue Head May Compete — Fair Lock]
FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// The lock has not yet been acquired
if (c == 0) {
// [Fair lock]: nobody in the queue is waiting ahead of me
// (the queue is empty or I am the queue head)
if (!hasQueuedPredecessors() &&
// CAS-set state and acquire the lock successfully
compareAndSetState(0, acquires)) {
// Set the lock-owning thread to the current thread
setExclusiveOwnerThread(current);
return true;
}
}
// The lock has already been acquired, and the owner is the current thread: reenter
else if (current == getExclusiveOwnerThread()) {
// Increase state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// Lock acquisition failed
return false;
}
2.2.4. If Lock Acquisition Fails, Join the Blocking Queue
AQS.addWaiter
private Node addWaiter(Node mode) {
// Construct a node with the current thread and EXCLUSIVE mode
Node node = new Node(Thread.currentThread(), mode);
// The queue is not empty
Node pred = tail;
if (pred != null) {
// Insert at the tail
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// The queue is empty or insertion at the tail failed
enq(node);
return node;
}
2.2.4.1. Enqueue Operation
enq
private Node enq(final Node node) {
// Infinite loop until enqueue succeeds
for (;;) {
Node t = tail;
// The queue is empty, so initialize the head.
// Note that this is new Node rather than the current node (the head is a placeholder)
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
// The queue is not empty; insert at the tail
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.2.5. Block and Wait to Be Woken to Continue Acquiring the Lock
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// Infinite loop until lock acquisition succeeds
for (;;) {
// Logic 1.
// When the predecessor of the current node is the head
// (fair lock: nobody is waiting for the lock ahead of me), try to acquire the lock
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// After acquiring the lock, set the head to the current node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// Logic 2.
// When the predecessor's state is SIGNAL (it promises to wake the current node),
// block the current thread.
// When is it woken? When the lock is released.
// What happens after it wakes? Continue the loop and execute Logic 1 above
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// When is this executed? When an exception causes lock acquisition to fail
if (failed)
cancelAcquire(node);
}
}
2.2.5.1. Determine Whether Blocking Is Needed
shouldParkAfterFailedAcquire
// Given (predecessor, current node) -> whether to block the current thread
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// If the predecessor is SIGNAL, it promises to wake the current node after releasing the lock.
// Return true so the current thread can block
if (ws == Node.SIGNAL)
return true;
// Predecessor state > 0 means CANCELLED.
// Walk backward to find an uncancelled predecessor, removing CANCELLED nodes from the list
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// Predecessor state >= 0, i.e. 0 or PROPAGATE.
// CAS-set the predecessor's state to SIGNAL; on failure it will retry. Why?
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.2.5.1.1. Block the Current Thread
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
// Use Unsafe underneath to block the current thread.
// This clears the thread's interrupt flag, so the interrupt state needs to be returned
LockSupport.park(this);
return Thread.interrupted();
}
2.3. Unlocking
public void unlock() {
// Call AQS.release
sync.release(1);
}
2.3.1. Release the Lock with AQS
release
public final boolean release(int arg) {
// Sync overrides tryRelease; release the lock through Sync
if (tryRelease(arg)) {
Node h = head;
// If the queue head is non-null and its state is normal, wake the head's successor
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
Sync overrides tryRelease, so this calls Sync.tryRelease.
The rest of the logic is the same as 5.AQS.md. Only the main logic is briefly described below.
2.3.2. Try to Release the Lock
Sync.tryRelease
protected final boolean tryRelease(int releases) {
// Unlock
int c = getState() - releases;
// Locking and unlocking must be done by the same thread
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// After the lock has been fully released, clear the owning thread
free = true;
setExclusiveOwnerThread(null);
}
// Set the state after unlocking
setState(c);
return free;
}
2.3.3. After Releasing the Lock Successfully, Wake a Node in the Blocking Queue
AQS.unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// If the current node's state < 0, change it to 0.
// 0 is an empty state because after the thread of this node releases the lock,
// there is nothing else it needs to do
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// The current node's next node is null or has state > 0 (cancelled)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// Traverse backward from the tail to find the nearest following node to the current node
// whose state <= 0 (not cancelled)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// Wake the next node (fair lock)
if (s != null)
LockSupport.unpark(s.thread);
}
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub