preface

ReentrantLock is a reentrant exclusive lock that resides in a JUC package and is a very common locking mechanism. Recently I also happened to learn ReetrantLock, feel this is still more complex, worth recording. This article mainly records and explains the process of obtaining and releasing ReentrantLock.

The structure of the lock

public class ReentrantLock implements Lock, java.io.Serializable { private static final long serialVersionUID = 7373984872572414699L; /** Synchronizer providing all implementation mechanics */ private final Sync sync; / /... public ReentrantLock() { sync = new NonfairSync(); } / /... public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } / /... public void lock() { sync.lock(); } / /... public void lockInterruptibly() throws InterruptedException { sync.lockInterruptibly(); } // Other functionsCopy the code

ReentrantLock has an internal variable Sync of type Sync. The above code lists some common operations that call the corresponding methods in Sync. As you can see, ReetrantLock implements these common methods primarily through the sync variable.

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
Copy the code

Then look at the Sync this class, I noticed it was ReetrantLock inner class, inherited the well-known AbstractQueuedSynchronizer (AQS) class. The main lock-related operations are implemented in this class. Let’s look at some specific operations.

Lock object creation

ReentrantLock = new ReentrantLock(); Statement to create a lock object. Take a look at the ReentrantLock constructor in the source code.

public ReentrantLock() {
    sync = new NonfairSync();
}
// other code
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code

As you can see, the constructor creates Sync’s object, Sync (NonfairSync and FairSync are both subclasses of Sync, representing unfair and fair locks, respectively). By default, an unfair lock is created with no arguments. If true is passed by fair, a fair lock is created.

Acquiring a lock

public void lock() {
    sync.lock();
}  
public boolean tryLock() {
    return sync.tryLock();
}
Copy the code

These two functions are the main ones for obtaining locks, along with lockInterruptibly() and tryLock(long timeout, TimeUnit Unit) that respond to interrupts and time limits. However, it is roughly the same as the normal lock() and tryLock() mechanisms, so I won’t introduce them. Take an example of an unfair lock process:

// Final void lock() {if (! initialTryLock()) acquire(1); } final Boolean initialTryLock() {Thread current = thread.currentThread (); if (compareAndSetState(0, 1)) { // first attempt is unguarded setExclusiveOwnerThread(current); return true; } else if (getExclusiveOwnerThread() == current) { int c = getState() + 1; if (c < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(c); return true; } else return false; } public final void acquire(int arg) {if (! tryAcquire(arg)) acquire(null, arg, false, false, false, 0L); } // Protected final Boolean tryAcquire(int acquires) {if (getState() == 0 && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; }Copy the code

As can be seen, a complete longest call relationship is: sync.lock()-> sync.initialtrylock ()->acquire(1)->tryAcquire(1)->acquire(many args). The detailed process is as follows: first call initialTryLock(), first try to use CAS mode to set state from 0 to 1 (state is a variable of the AQS class, used to indicate whether the lock was acquired, naturally inherited by Sync class), if successful, set the owning thread of the lock to this thread; Otherwise, check whether the thread already owns the lock. If so, set state to the number of reentrantlocks. Otherwise, return false.

Next, acquire() of the AQS class is called, starting with the tryAcquire() function, which is overridden by NonfairSync. Here we check again to see if the lock is released, if so, get it directly, otherwise return false.

If all of the above attempts return false, then the lock will definitely not be acquired for a while. Call AQS acquire() with many arguments, which will put the thread into the lock’s blocking queue. AQS, I won’t go into it here.

The above describes the process of obtaining a lock for an unfair lock. Fair and unfair locks implement the initiTryLock() and tryAcquire() methods, respectively. For a fair lock, the rest of the code for acquiring the lock is the same as for an unfair lock, except that when a thread is set to acquire the lock, there is one more! HasQueuedThreads () checks, which is a method in AQS that checks if any threads are blocked before it. This reflects the so-called fair and unfair: unfair lock does not check whether there is a thread in front of it, as long as it finds the lock can be acquired directly, so unfair (you wait for the lock, but it happens to come late and take the lock away), fair lock is more ‘fair’.

Then look at tryLock () :

final boolean tryLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (getExclusiveOwnerThread() == current) {
        if (++c < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(c);
        return true;
    }
    return false;
}
Copy the code

This method is implemented in the Sync class, so the lock can be acquired regardless of whether the lock is fair or not. The code is similar to initialTyeLock(), so I won’t explain it. As you can also see, tryLock() does not add the thread to the blocking queue, and returns false on failure.

The release of the lock

Public void unlock() {sync.release(1); Public final Boolean release(int arg) {if (tryRelease(arg)) {signalNext(head); return true; } return false; } // In Sync protected final Boolean tryRelease(int releases) {int c = getState() -releases; if (getExclusiveOwnerThread() ! = Thread.currentThread()) throw new IllegalMonitorStateException(); boolean free = (c == 0); if (free) setExclusiveOwnerThread(null); setState(c); return free; }Copy the code

To release the lock, the release(1) function in AQS is first called, followed by the tryRelease() function overwritten by Sync, where the reentrant number of the lock is reduced by one. If state changes to 0, the lock is released. To release the lock setExclusiveOwnerThread(null); . If the lock is released, the signalNext() function of AQS is then called. Notify other threads that the lock is available. It is worth noting that tryRelease() is not the same as tryAcquire(), which is implemented by both fair and unjust locks, whereas tryRelease() is implemented in Sync, which is shared by both fair and unjust locks. That’s because releasing the lock doesn’t matter if it’s fair or not.

Create a conditional object

Another common method of ReentrantLock is to create conditional objects, which is also very simple in the source code.

public Condition newCondition() { return sync.newCondition(); ConditionObject newCondition() {return new ConditionObject(); }Copy the code

The newCondition() method and the main operators of ConditionObject are both defined in the AQS class and won’t be covered here.

The blocking and conditional queues corresponding to a ReentrantLock object are illustrated above. I will write down the relevant AQS when I have time.

Lock status state

The getState() method is often used in the lock acquisition and release code above. This method actually returns the state variable. State is a variable of the AQS class that, in ReentrantLock, identifies the number of times the lock is reentrant and whether it is held. Here are some ways to do it:

private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    return U.compareAndSetInt(this, STATE, expect, update);
}
Copy the code

These methods are in the AQS class. First, state is a volatile variable, ensuring that each thread reads the state value that was last updated by the thread. The get and set methods are simpler and won’t be explained. Finally, there is the compareAndSetState() method: as the name implies, update the value of state with CAS. The broadening class, which resembles a pointer that directly manipulates data for the address, is used here to update:

private static final Unsafe U = Unsafe.getUnsafe(); / / retrieve attributes offset private static final long STATE = u.o. bjectFieldOffset (AbstractQueuedSynchronizer. Class, "the STATE");Copy the code

The thread that holds the lock

SetExclusiveOwnerThread () and getExclusiveOwnerThread() are mentioned in the above lock acquisition and release methods. Let’s talk about the thread that holds the lock.

/ / the AbstractOwnableSynchronizer class protected final void setExclusiveOwnerThread (Thread Thread) {exclusiveOwnerThread = thread; } protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; }Copy the code

The above mentioned Sync inherited AQS, and AQS class inherits the AbstractOwnableSynchronizer class, Has a property in the AbstractOwnableSynchronizer class private transient Thread exclusiveOwnerThread; Is used to identify which thread holds the lock. So the set and get methods above actually operate on this property.

protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); Final Thread getOwner() {return getState() == 0? null : getExclusiveOwnerThread(); }Copy the code

Based on this property, there are other methods in ReentrantLock such as the two above that are relatively simple.

conclusion

This article explores and documents the common approaches to ReentrantLock at the source level. However, the operations related to AQS are directly omitted, and the author will study the AQS category later.