Foreword
Note: The source code of this article comes from JDK11
ReentrantLock
is a reentrant lock in Java, which can be used to replace the traditional synchronized
keyword for thread synchronization.
Here are some comparisons with the synchronized
keyword:
name | implementation | reentrancy | interruption | fairness | Does it support timeout | Release lock |
---|---|---|---|---|---|---|
ReentrantLock | Java API level , based on AQS implementation | reentrant | interruptible | support unfair and fair | timeout | Whether to lock manually |
synchronized | JVM level, object-based monitor object implementation | reentrant | Uninterruptible | Unfair | Unable to set timeout | Automatic lock |
It can be seen that ReentrantLock
provides more flexibility and scalability. I wonder if you are interested in its principle?
Note: You need to have some understanding of how AQS works, because ReentrantLock is implemented on top of AQS.
Class structure
The ReentrantLock
class implements the java.util.concurrent.locks.Lock interface, and its internal implementation includes a Sync
inner class, which is ReentrantLock
core implementation.
Sync
inherits the AbstractQueuedSynchronizer
abstract class, which is used to manage the synchronization status of threads. The Sync
class has two subclasses, NonfairSync
and FairSync
, which are used to implement unfair locks and fair locks. UML is shown in the following figure:
Sync class source code
Since fair locks and unfair locks are subclasses of Sync
, we first analyze the Sync
class
abstract static class Sync extends AbstractQueuedSynchronizer {<!-- --> private static final long serialVersionUID = -5179523762034025860L; /** * The default unfair implementation, there is a detail, why should the unfair implementation be placed here in the parent class? * In fact, ReentrantLock's tryLock() is directly called here, no matter which implementation you are, it is an unfair implementation. */ @ReservedStackAccess final boolean nonfairTryAcquire(int acquires) {<!-- --> final Thread current = Thread. currentThread(); int c = getState(); // If the state is 0, it means that no thread is holding the lock if (c == 0) {<!-- --> // cas success means getting the lock if (compareAndSetState(0, acquires)) {<!-- --> // Set the thread that currently acquires the lock setExclusiveOwnerThread(current); return true; } } // Because it is a reentrant lock, when the state is not 0, then judge if it is the current thread, then add the state, and then subtract it when the lock is released else if (current == getExclusiveOwnerThread()) {<!-- --> int nextc = c + acquires; // From here we can know that the number of reentrants is limited, which is 2147483647 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // Since it is its own thread, there can be no competition, so there is no need to use cas setState(nextc); return true; } return false; } /** * The logic of releasing the lock is the same for unfair and fair, because the lock has been acquired, so the code here is thread-safe and does not use cas */ @ReservedStackAccess protected final boolean tryRelease(int releases) {<!-- --> int c = getState() - releases; if (Thread. currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {<!-- --> free = true; setExclusiveOwnerThread(null); } setState(c); return free; } // slightly... }
FairSync (fair lock source code)
Lock
Let’s first look at the locking logic of the fair lock. Here we only need to implement tryAcquire
, because the parent class Sync
has already implemented the tryRelease
method.
static final class FairSync extends Sync {<!-- --> private static final long serialVersionUID = -3000897897090466540L; /** * The tryAcquire method of the fair lock is almost the same as the unfair implementation here, except that the judgment of hasQueuedPredecessors() is added */ @ReservedStackAccess protected final boolean tryAcquire(int acquires) {<!-- --> final Thread current = Thread. currentThread(); int c = getState(); if (c == 0) {<!-- --> // hasQueuedPredecessors() indicates whether there are threads waiting in the queue before the current thread // true: head --> node(thread1) --> node(currentThread) // false: head --> node(currentThreaad) // Because fairness must be guaranteed, it is only necessary to follow the FIFO queue of AQS. If the current thread is not at the head of the queue, it cannot obtain the lock and return false if (!hasQueuedPredecessors() & amp; & amp; compareAndSetState(0, acquires)) {<!-- --> setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) {<!-- --> int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
Unlock
Unlock directly uses the parent class, so there is no need to rewrite.
NonfairSync (unfair lock source code)
Lock
Let’s first look at the locking logic of unfair locks, which is very simple.
static final class NonfairSync extends Sync {<!-- --> private static final long serialVersionUID = 7316153563782823691L; protected final boolean tryAcquire(int acquires) {<!-- --> // Directly use the method of the parent class, which has been analyzed above, please pull it back and have a look return nonfairTryAcquire(acquires); } }
Unlock
The same unlocking is directly used by the parent class without rewriting.
Why are unfair locks more efficient than fair locks?
In many cases, it is not recommended to use fair locks to lock. It is said that the efficiency of using unfair locks is better than that of fair locks, but why?
Judging from the above source code, the fair lock is only judged by hasQueuedPredecessors()
. In fact, context switching is involved when waking up the blocked thread in the queue, which requires a certain amount of time consumption. Multiple The time-consuming accumulation of threads is naturally less efficient.
It is also very simple to minimize the time of context switching, just let the current thread grab the lock directly, because the current thread is in user mode, there will be no time-consuming context switching, and the efficiency will naturally be good.
Summary
This article compares the difference between ReentrantLock
and synchronized
, knowing that ReentrantLock
is more flexible in usage; it analyzes the fairness of ReentrantLock
The implementation of locks and unfair locks found that the code is very simple, which all benefited from the abstract synchronization framework of AQS. In fact, the main difference between them is whether they will try to acquire locks when locking; finally think about why it is unfair Locks are more efficient than fair locks.