ReentrantLock source code analysis

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:

Reentrant lock uml

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.