Details of java concurrent reentry locking ReentrantLock

  • 2021-07-06 10:52:35
  • OfStack

Preface

At present, there are two mainstream locks, one is synchronized, and the other is ReentrantLock. Up to now, the performance of synchronized is equal to that of reentrant lock, but the function and flexibility of reentrant lock are much more than this keyword, so reentrant lock can completely replace synchronized keyword. Let's introduce this reentry lock.

Text

ReentrantLock reentrant lock is the most important implementation in Lock interface, and it is also the most widely used one in actual development. My article is closer to the actual development application scenario, providing developers with direct application. So I don't explain all the methods, and I won't introduce or mention some unpopular methods.

1. First look at which constructors are required to declare a reentry lock


public ReentrantLock() {
        sync = new NonfairSync();
    }
 
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

Recommended declaration method


private static ReentrantLock lock = new ReentrantLock(true);
private static ReentrantLock locka = new ReentrantLock();

Highlights:

ReentrantLock provides two constructions corresponding to two declarations.

The first type declares fair lock. The so-called fair lock means that the thread that waits first gets the lock first in chronological order. Moreover, fair lock will not produce hungry lock, that is, as long as it waits in line, it can finally wait for the opportunity to get the lock.

The second kind of declaration is unfair lock. The so-called unfair lock is contrary to the concept of fair lock. The order in which threads wait is not necessarily the order of execution, that is, the threads that come in later may be executed first.

ReentrantLock is a non-fair lock by default, because fair lock realizes the fairness of first-in, first-out. However, because one thread joins the queue, it often needs to block, and then changes from blocking to running. This context switching is very good performance. Unfair lock allows queue jumping, so the context switching is much less, better performance, guaranteed large throughput, but prone to starvation. Therefore, in actual production, unfair locks are often used.

The unfair lock calls the NonfairSync method.

2. What does the lock method do after adding locks (only unfair locks)
Just now, we said that if the lock is not fair, the NonfairSync method will be called, so let's see what this method does.


static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
 
        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
 
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

Highlights:

Pre-read prophet: ReentrantLock uses state to indicate "the number of times the thread holding the lock has repeatedly acquired the lock". When state (replaced by state 2 subs below) equals 0, no thread currently holds a lock).
Step 1 calls the compareAndSetState method, passing that the first parameter is the expected value 0, and the second parameter is the actual value 1. At present, this method actually calls unsafe. compareAndSwapInt to realize CAS operation, that is, the state must be 0 before locking. If it is 0, call setExclusiveOwnerThread method


private transient Thread exclusiveOwnerThread;
 
    
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

It can be seen that setExclusiveOwnerThread means that the thread is set to the current thread, which means that one thread has already got the lock. Everyone is CAS has 3 values. If the old value is equal to the expected value, the new value will be given, so the current thread will set the state to 1 when it gets the lock.

Step 2 is when the compareAndSetState method returns false, the acquire method is called at this time, and the parameter is passed to 1

The tryAcquire () method actually calls the nonfairTryAcquire () method.


public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
 
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

It is very clear from the comment that the exclusive lock is requested, all interrupts are ignored, tryAcquire is executed at least once, and if it succeeds, it will return, otherwise the thread will enter the blocking-waking two-state switch until tryAcquire succeeds. See links tryAcquire (), addWaiter () and acquireQueued () for details.

Well, until a few days ago, everyone has made clear the process from lock () method to call, and made clear why only the current thread that gets the lock can execute, and those that don't get it will constantly use CAS principle to try to get the lock in the queue. CAS is very efficient, that is, why ReentrantLock is more efficient than synchronized, and the disadvantage is that it wastes cpu resources.

3. Call the unlock () method after all threads have finished executing

The unlock () method is implemented by the release (int) method of AQS. We can look at 1:


public void unlock() {
        sync.release(1);
    }
 
 
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease () is implemented by a subclass, so let's look at the implementation of Sync in ReentrantLock under 1:


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;
        }

First, obtain the status identification through getState. If this identification is equal to the number to be released, the thread currently occupying the lock will be set to null to release the lock, and then return to true. Otherwise, subtract releases from the status identification and return to false.


Related articles: