What is already

A ReentrantLoock is also known as a reentrant lock. Reentrant means that a thread that already holds the lock can repeatedly acquire the lock, so that if the lock does not support reentrant, the thread that holds the lock will be blocked when it relocks. If a lock is acquired in a recursive method, the recursion will be blocked by the lock request.

The knowledge of locks is divided into two parts, one part is how to add locks and the other part is who to assign locks to. ReentrantLoock relies on AQS, which solves the problem of who to assign the lock to, while ReentrantLoock only needs to figure out how to add the unlock.

The principle of AQS can be found in the article to understand AQS. Knowing AQS makes ReentrantLock easy, and not knowing it does not hinder understanding.

The features of ReentrantLock are:

  • reentrant
  • For an exclusive lock, only one thread can acquire the lock at a time
  • Support fair and unfair ways to acquire locks. Fair means that locks are allocated in the order in which they are applied
  • It inherits other features of AQS

ReentrantLock implements Lock semantics, and the Lock interface represents the method template that the Lock should have

public interface Lock {
    void lock(); // Get the lock. If you don't get the lock, it will block
    void lockInterruptibly() throws InterruptedException; // Get the lock, can be interrupted, not get the lock will be blocked
    boolean tryLock(); // Get the lock, no matter what the result will not be blocked
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // Get lock, return result in unit time at most, can be interrupted
    void unlock(); / / releases the lock
    Condition newCondition(); // It supports locking after certain conditions are met
}
Copy the code

AQS basis

Assuming that we have seen the controls for adding locks, let’s take a quick look at how AQS assigns locks:

  1. When a lock is requested, that is, a semantically similar method like acquire() is called,AQS will ask the subclass if the lock was successful and continue running if it was. Otherwise, AQS logs the lock request at Node granularity, inserts it into its own maintained CLH queue and suspends the thread
  2. In the CLH queue, only the node closest to the head node that has not unclaimed the lock is eligible to apply for the lock
  3. When the thread is woken up, it attempts to acquire the lock, and continues to suspend if it does not. Get it and continue running
  4. When a thread releases the lock, that is, calls the semantically similar method release(), AQS will ask the subclass if the lock has been unlocked successfully, and if so, AQS will actively invoke the appropriate thread from the CLH queue, in procedures 2 and 3
  5. If you need to wait for a condition to be satisfied before applying for a lock, that is, if a semantically similar method wait() is invoked, AQS will maintain a one-way queue of waiting conditions with Node as the granularity, and suspend the thread represented by Node
  6. When the condition is met, that is, when the semantically similar method signal() is called, wake up the Node at the top of the waiting condition queue and execute 1
  7. Subclasses can maintain the state property of AQS to record the unlock state. AQS also provides the CAS method compareAndSetState() to preempt the update state

Briefly, when AQS assigns a lock, the current thread may be suspended and then woken up to try again, repeating the process until the lock is acquired or the wait is cancelled. Externally, it looks as if the entry method was blocked and restored in the future.

Sync

Now you can see how ReentrentLock works. In ReentrentLock, the internal Sync class inherits AQS, and the subsequent fair and unfair locks are also based on Sync subclasses. The main methods of Sync are nonfairTryAcquire() and tryRelease().

        final boolean nonfairTryAcquire(int acquires) {
            // The current thread
            final Thread current = Thread.currentThread();
            // The number of times it was locked
            int c = getState();
            if (c == 0) {
                // Indicates that no thread has acquired the lock
                if (compareAndSetState(0, acquires)) {
                    // Lock times set to acquires times through CAS
                    // Remember the thread that took the lock
                    setExclusiveOwnerThread(current);
                    // Lock succeeded
                    return true; }}else if (current == getExclusiveOwnerThread()) {
                // The number of times that the lock is held by a thread is not 0
                // The number of locks to update
                int nextc = c + acquires;
                // The number of locks can never be less than 0. If it is less than 0, a concurrency error has occurred
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // Update the lock count
                setState(nextc);
                // Lock succeeded
                return true;
            }
            // Failed to lock
            return false;
        }
Copy the code

NonfairTryAcquire () acquired the lock in an unfair manner, and when the thread holding the lock applied for it again, it could be locked again, reentrant occurred. It is worth noting that the number of locks updated by setState() in this method is, and concurrency is not a concern, because this is continuous behavior occurring on the same thread.

        protected final boolean tryRelease(int releases) {
            // After the lock is released, a new number of locks must be added
            int c = getState() - releases;
            if(Thread.currentThread() ! = getExclusiveOwnerThread())// The thread that is not applying for the lock cannot release the lock
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                After release, no lock is held by the thread
                // If the lock is not entered, it means that the lock is still held by the current thread
                free = true;
                // There is no thread holding the lock
                setExclusiveOwnerThread(null);
            }
            // Update the number of locks
            setState(c);
            // True means to tell AQS that there is a lock to continue allocation, false means to tell AQS that there is no lock to allocate
            return free;
        }
Copy the code

TryRelease () is a method that AQC requires subclasses to implement to ask if the lock has been unlocked and can continue to be allocated. As with nonfairTryAcquire() before, there is no need to worry about concurrency and only the locked thread will call it.

Other methods are briefly described as follows:

  • Lock (): Lock entry method to be implemented by subclasses
  • IsHeldExclusively () : Checks whether the thread holding the lock is the current thread
  • NewCondition () : returns an entry operation object supported by AQS that allows threads to wait for conditions to apply for locks again
  • GetOwner () : Returns the thread currently holding the lock
  • GetHoldCount () : indicates the number of locks held
  • IsLocked () : Whether any threads hold locks
  • ReadObject () : Restores Sync from serialization

ReentranLock defaults to an unfair lock, and you can instantiate method parameters to determine whether to use a fair or unfair lock, both of which are subclasses of Sync.

Unfair lock implementation

The implementation of unfair locks is relatively simple, Sync already supports nonfairTryAcquire() to support acquiring locks unfairly, The implementation of NonfairSync, which represents unfair locks, only needs to maintain the unfair characteristics of the first AQS request for lock allocation.

    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0.1))
                // select * from AQS
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // Request allocation from AQS
                acquire(1);
        }

        // This method is AQS request subclass implementation, and ask subclass to lock the method
        protected final boolean tryAcquire(int acquires) {
            // the Sync method is called directly
            returnnonfairTryAcquire(acquires); }}Copy the code

TryAcquire () is a method for AQS that asks subclasses to implement and asks them if they successfully locked. (1), if the lock can be locked, there is no need to bother AQS to assign the lock, and the lock state is also saved by CAS. So, you can cut in line here. Imagine that at some point in the AQS there are thread nodes waiting for the lock to be allocated. At this point, a thread releases the lock, and the time slice is allocated to the thread that just called lock(). Then the thread takes the lock and interrupts the queue. The same situation has a chance to happen through AQS acquire().

Implementation of fair locks

Unfair locking is implemented by FairSync, a subclass of Sync.

    static final class FairSync extends Sync {

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            // The current thread
            final Thread current = Thread.currentThread();
            // The number of locks
            int c = getState();
            if (c == 0) {
                // ① No thread holds the lock
                if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    There is no thread in the queue waiting for the lock to be allocated. 2. The lock was obtained because CAS updated the lock count successfully
                    setExclusiveOwnerThread(current);
                    // Tell AQS to get the lock
                    return true; }}else if (current == getExclusiveOwnerThread()) {
                // Indicates the current thread is reentrant
                // The number of locks to update
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // Update lock times
                setState(nextc);
                // Tell AQS that the lock was successful
                return true;
            }
            // tell AQS the lock failed
            return false; }}Copy the code

In ①, FairSync handles queue-jumping. When FairSync uses acquire() to assign a lock to AQS, AQS will first use tryAcquire() to assign a lock to FairSync and then queue the thread that has requested the lock. But FairSync needed to maintain the fair feature, so checking hasqueuedToraise () found that a Node with representative threads waiting for a lock failed to directly return to add the lock, eliminating queue-jumping.

conclusion

Other method semantics of Lock interface, ReentranLock does not encapsulate directly using AQS features can be realized, so do not expand. ReentranLock can be used according to the interface semantics of Lock. The simplest methods to unLock ReentranLock are lock. Lock() and lock. unLock().

So, the point of ReentranLock is to extend the AQS to achieve reentrant, fair or unfair characteristics.

  • When AQS fails to apply for a lock from its subclass Sync, Sync determines whether the thread applying for the lock is the thread holding the lock. If so, it increases the number of locks and allows the lock to pass, thus reentrant occurs. Then, the extra lock times caused by reentrant can only be released by the locked thread.
  • A fair or unfair implementation is that when AQS requests a lock, AQS requests a lock from its subclass Sync, and if it fails, the lock request is queued in the CLH queue. Sync can then determine whether the lock is fair by directly telling AQS if the lock failed when it detects that there are threads queuing in the CLH queue. In NonfairSync, it ignores the queue in AQS and tries to jump the queue directly. With FairSync, it does not lock directly when it finds that the queue has nodes waiting on behalf of the thread applying for the lock.

To sum up, ReentranLock does not have too many special operations, nor does it have its own rigid technical points. That is to say, the level of understanding of ReentranLock depends on the level of understanding of AQS. Not only ReentranLock, but many locks in Java also rely on AQS. Therefore, learning ReentranLock is not enough; you need to take AQS further to get to the core of Java’s many concurrency secrets.

ASQ learns the portal