/ Java  

Java Multithreading 17: Fair lock - Release

In the previous blog, we learned the process of acquiring locks of fair locks. Now, let’s take a look at the process of releasing locks of fair locks.

Release fair lock (based on JDK 11.0.5)

1. unlock()
unlock() is implemented in ReentrantLock

1
2
3
public void unlock() {
sync.release(1);
}

unlock() is the unlock function, which is implemented through AQS‘s release() function.

Here, the meaning of 1 is the same as the meaning of the function acquire(1) that acquires the lock, which is the parameter that sets the state of releasing the lock. Since the fair lock is reentrant, for the same thread, the state of the lock is -1 every time the lock is released.

The relationship between AQS, ReentrantLock and sync is as follows:

1
2
3
4
5
6
7
8
9
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}

// ...
}

sync is a member object in ReentrantLock, and Sync is a subclass of AQS.

2. release()
release() is implemented in AQS

1
2
3
4
5
6
7
8
9
10
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}

return false;
}

release() will first call tryRelease() to try to release the lock held by the current thread. If successful, wake up the subsequent waiting thread and return true. Otherwise, directly return false.

3. tryRelease()
tryRelease() is implemented in the Sync class of ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final boolean tryRelease(int releases) {
// c is the state after the lock is released this time
int c = getState() - releases;

// If the current thread is not the lock holder, an exception is thrown!
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();

boolean free = false;
// If the lock has been completely released by the current thread, set the holder of the lock to null, that is, the lock is available for acquisition.
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}

// Set the lock status of the current thread.
setState(c);
return free;
}

The purpose of tryRelease() is to try to release the lock.

  • If current thread is not lock holder, then throw an exception.
  • If the current thread owns the lock after the current lock release operation is 0 (that is, the current thread completely releases the lock), then the holder of the lock is set to null. So the lock Is available. At the same time, update the lock status of the current thread to 0.

getState(), setState() have been introduced in the previous blog so we will skip them here.

getExclusiveOwnerThread(), setExclusiveOwnerThread() is defined in AbstractOwnableSynchronizer, the parent class of AQS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
// The current lock owner thread
private transient Thread exclusiveOwnerThread;

// Sets the thread that currently owns exclusive access.
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

// Returns the thread last set by setExclusiveOwnerThread, or null if never set.
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}

4. unparkSuccessor()
If the current thread releases the lock successfully in release(), it will wake up the successor thread of the current thread.

According to the FIFO rules of the CLH queue, the current thread (that is, the thread that has acquired the lock) must be the head. If the CLH queue is not empty, the next waiting thread of the lock is awakened.

Let’s take a look at the source code of unparkSuccessor(), which is implemented in AQS.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;

if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);

/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}

// awaken the successor thread
if (s != null)
LockSupport.unpark(s.thread);
}

unparkSuccessor() is to wake up the successor thread of the current thread. After the subsequent thread is woken up, it can acquire the lock and resume running.

Summary

The process of releasing the lock is relatively simple compared to the process of acquiring the lock. When releasing the lock, the main operation is to update the state of the lock corresponding to the current thread. If the current thread has completely released the lock, set the lock holding thread to null, set the current thread’s state to empty, and then wake up the subsequent thread.