Implementation of internal principle of ReentrantLock

Posted by adzie on Thu, 04 Nov 2021 23:38:45 +0100

ReentrantLock is implemented based on AQS (AbstractQueuedSynchronizer). It is divided into fair lock and unfair lock. ReentrantLock can be set as a fair lock by entering and leaving true in the constructor.

AQS is a FIFO two-way queue, which internally records the queue head and tail elements through nodes head and tail. The type of queue element is Node, in which the thread variable in Node is used to store the thread entering the AQS queue, and the SHARED in Node is used to mark that the thread is blocked and suspended to obtain SHARED resources and put into the AQS queue, EXCLUSIVE is used to mark that the thread is put into the AQS queue after being bureaucratic when obtaining EXCLUSIVE resources. waitStatus records the current thread waiting status, which can be CANCELLED (thread is CANCELLED), SIGNAL (thread needs to be awakened), CONDITION (thread waits in the CONDITION queue), and PROPAGATE (other nodes need to be notified when releasing SHARED resources); prev records the predecessor nodes of the current Node, and next records the successor nodes of the current Node.

For ReentrantLock, AQS state is used to record the number of reentrant times that the current thread obtains the lock. Different tool classes have different meanings. For example, ReentrantReadWriteLock indicates the read state in the upper 16 bits of state, that is, the number of reads to obtain the lock, and the lower 16 bits indicate the number of reentrant times of the thread that obtains the write lock

AQS also mainly provides four methods: tryAcquire, tryacquisseshared, tryRelease and tryrereleaseshared, which are implemented by subclasses

The main logic of ReentrantLock is as follows:

Unfair lock

1. new ReentrantLock() creates a non fair lock. The internal implementation is to create an internal class named sync, NonfairSync

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

2. Call the lock method of ReentrantLock and internally call the lock method of sync. The method semantics is: change the AQS state from 0 to 1 through cas

    public void lock() {
        sync.lock();
    }
final void lock() {
            if (compareAndSetState(0, 1))
                ...
            else
                ....
        }

3. If the second step is successful, set the exclusive thread exclusiveOwnerThread in AQS to itself

final void lock() {
            if (compareAndSetState(0, 1))
            	//The following code
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

4. If the second step fails, call the acquire method of AQS, that is, the exclusive lock acquisition method, to obtain a resource

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	//The following code
                acquire(1);
        }

5. Continue to call the nonfairTryAcquire method in ReentrantLock, that is, the implementation of unfair lock acquisition

    public final void acquire(int arg) {
    	//The following code
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

6. The internal semantics of the method is: get the state. If it is equal to 0, try to change it to 1, and set the exclusive thread ID

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	//The following code
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                ...
            }
            return false;
        }

7. Otherwise, it is determined whether the exclusive lock ID is itself. If so, it is re entered and the state is increased by 1

        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()) {
            	//The following code
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

8. If they are not successful, proceed to the next operation

9. Add a new node. Create a new node for the parameter by the current thread and add it to the end of the linked list

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
        	//The addWaiter method in the following code
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

10. Call the acquirequeueueueued method of AQS

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
        	//The acquirequeueueueued method in the following code
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

11. Loop inside the method to obtain the previous node through the newly added node

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //loop
            for (;;) {
            	//The following code
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

12. If the previous node is the head node, try to call the tryAcquire method to obtain the lock. After successful acquisition, set yourself as the head node and remove the thread attribute associated with the node

if (p == head && tryAcquire(arg)) {
 	setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

13. If it is not the head node or the lock acquisition fails, try to modify the node status waitStatus and block its own thread

//Methods: shouldParkAfterFailedAcquire modify status, parkAndCheckInterrupt block
 if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

14. Because it is a cycle, all nodes marked as cancelled are removed and those in other states are modified to wait. However, it can be seen from step 13 that each subsequent node is only responsible for modifying the state of its own precursor node, because after the modification is successful, its own thread is blocked and waiting for wake-up.

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

Topics: Java lock