Triple entry lock in JAVA

Posted by systemick on Thu, 04 Nov 2021 17:00:17 +0100

In the previous article, I wrote about exclusive locks and shared locks. This time, I'll talk about reentry locks.

Reentry means that any thread can acquire the lock again after acquiring the lock without being blocked by the lock. Based on this definition, two problems will arise:

  1. The thread acquires the lock again. The lock needs to identify whether the thread obtaining the lock is the thread currently occupying the lock. If so, it will be obtained successfully again. This will involve the fairness and unfairness of lock acquisition, which will be mentioned below.
  2. Final release of the lock. The thread repeatedly acquires the lock N times, and then other threads can acquire the lock after releasing the lock N times.
    We know that synchronized supports implicit reentry. If recursion occurs in the synchronized modified method, the thread will not block itself. ReentrantLock achieves lock acquisition and release by combining a custom synchronizer, which is implemented in an unfair (default) manner.

Important internal classes
There are three important internal classes in ReentrantLock, namely Sync, NonfairSync and FairSync:

  1. Sync is the parent class of the latter two and inherits to AbstractQueuedSynchronizer.
  2. Both NonfairSync and FairSync inherit from Sync.
  3. NonfairSync is mainly used to implement non fair locks, and FairSync is mainly used to implement fair locks.

Important attributes
private final Sync sync;
Initialize in the construction method, and decide whether to use fair lock or unfair lock through the construction method parameters.

Important constructors

//Unfair lock
public ReentrantLock() {
    sync = new NonfairSync();
}

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

The difference between fair and unfair locks
Fairness and are for obtaining locks. If a lock is fair, the acquisition order of locks should conform to the absolute time order of requests, that is, FIFO.

Let's compare the codes obtained by fair lock and non fair lock:

    static final class NonfairSync extends Sync {
        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         * A non fair lock is different from a fair lock in that it sets the CAS status first and succeeds if it succeeds
         * Failed to join the waiting queue
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

        //Get unfair lock
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //The number of times a lock was acquired by the same thread
            //If the same thread is not released and it succeeds again, the number of times will be accumulated
            int c = getState();
            if (c == 0) {//No thread has acquired the lock yet
                if (compareAndSetState(0, acquires)) {
                    //Sets the current thread as the thread of the exclusive lock
                    setExclusiveOwnerThread(current);
                    return true;//Get success
                }
            }
            //Determine whether the current thread is the thread that has acquired the lock
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;//Get times accumulation
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;//Get success
            }
            return false;
        }

    //Fair access lock    
    static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //The only difference from unfairness is that the current node is added to the synchronization queue
                //Whether there is a precursor node. If yes, it means that a thread requests to acquire a lock earlier than the current thread,
                //You need to wait until the previous thread acquires the lock and releases it before continuing to acquire the lock.
                if (!hasQueuedPredecessors() &&
                    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;
        }
    }

Then look at the release of the lock:

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

If the lock is acquired N times, false will be returned during the first (N-1) release, and true will be returned only after the synchronization state is completely released.

Summary of fair lock and unfair lock

  1. If an unfair lock is set, CAS will set the status first. If the setting is successful, it means that the lock is obtained successfully. If it fails, it will be added to the waiting queue. Under this premise, the thread that has just released the lock will have a very high probability of acquiring the lock at this time.
  2. In the process of obtaining a fair lock, it will judge whether the current node joining the synchronization queue has a precursor node, which is one of the differences from obtaining a non fair lock.
  3. Fair lock follows FIFO and can avoid extreme hunger.
  4. Non fair lock can reduce thread context switching, and the throughput is higher than fair lock.

Reference article: ① Fair lock and unfair lock of ReentrantLock

Principle analysis of Java reentrant lock

Topics: Java