ReetrantLock Source Profiling

ReentrantLock Source Profiling

Here's another ReetrantLock series I forgot to read. Today I take the time to record the learning process of the ReentrantLock source code.This blog mainly records the following aspects.Welcome to your suggestions or comments

(1) Inheritance structure of ReetrantLock and Sync

2. ReetrantLock constructors and core properties of AQS

3. Examples of using ReetrantLock locks

4. ReetrantLock's Principle, Core Method and Design Ideas

1. Inheritance structure of ReetrantLock and Sync

The diagram above shows the inheritance structure and relationship of ReetrantLock and Sync. Within ReetrantLock, there is a member variable Sync, which in turn inherits from AQS (AbstractQueuedSynchronizer).There are also two internal classes FairSync and NonfairSync (both subclasses of Sync) inside ReetrantLock, and only two methods inside the two classes are methods that override the parent class, lock() and tryAcquire().

2. ReetrantLock constructors and core properties of AQS
2.1 Introduction to simple constructors
// Common constructors that create unfair locks internally
public ReentrantLock() {
        sync = new NonfairSync();
}

// By manually specifying whether the lock is fair or unfair, true is fair, and false is unfair
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}
2.2 Introduction to the attributes and related genera of ReetrantLock
  • ReentrantLock own properties
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
  • Core properties of AQS

    Before introducing the properties of AQS, the structure and usage of AQS is introduced. AQS is the core of many synchronizers, and CAS mechanism is used extensively inside.AQS is the basis for most synchronization needs.

    The main use of synchronizers is inheritance, and subclasses manage synchronization state by inheriting and implementing their abstract methods.The core of synchronizer components, such as ReentrantLock, ReentrantReadWriteLock, and CountDownLatch, are AQS.

    AQS uses queues to manage threads waiting for locks, similar to a FIFO queue inside, where each node is represented by a Node.The structure is illustrated below.

// Attributes under AQS are core attributes of synchronizer
private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

// Attributes in the Node structure, this time only the attributes related to ReetrantLock are analyzed
static final class Node {
        // Exclusive lock mode
        static final Node EXCLUSIVE = null;

        // Cancel status
        static final int CANCELLED =  1;
        // This state indicates that it has subsequent nodes
        static final int SIGNAL    = -1;
      	// The wait state, which is assigned to one of the above states
        volatile int waitStatus;
        // Pre-Node
        volatile Node prev;
	    // Postnode
        volatile Node next;
	    // The thread on which the node depends
        volatile Thread thread;

}
3. Examples of using ReetrantLock locks

Following is a demonstration of a common example that we use to expand on the core principles of analysis.

public class Test{
    public static void main(String [] args){
        // Create Lock
        ReentrantLock myLock = new ReentrantLock();
        // Lock where needed
        myLock.lock();
        try {
            // Do something interesting after locking
            System.out.println("fuhang do something");
            throw new RuntimeException("Oh,that's too bad !");
        }catch (Exception e){
            // do something
        }finally {
            // Last but not least, release the lock manually
            lock.unlock();
        }
    }
}

Above is a simple example of ReetrantLock, where Interpolation records the advantages of a ReetrantLock over a Synchronized keyword, such as A thread acquiring a B lock, acquiring a B lock after acquiring it, and releasing a B lock after acquiring a C lock. In this scenario, ReentrantLock is a good control, whereas Synchronized is not so convenient.

4. Principle, core method and design idea of ReetrantLock

Below we'll take a step-by-step look at how ReetrantLock works using the methods in the example.

4.1 Locking process

(1) Call the ReetrantLock.lock() method first, which calls the lock method of the sync Attribute Variable internally

// ReetrantLock calls the lock method of a subclass of Sync, which is the lock method of the NonfairSync class here
public void lock() {
     sync.lock();
}

(2) Call the lock() method of NonfairSync, a subclass of the Sync class.

The first if condition of this method attempts to acquire a lock, where acquiring a lock means using the CAS mechanism to change the state attribute variable in the AQS class from 0 to 1 (the state attribute of Sync in ReentrantLock is 0), which means no lock, greater than 0When the modification is successful, the lock is acquired and the thread to acquire the lock is set by calling the AbstractOwnableSynchronizer.setExclusiveOwnerThread(Thread owner) method.

If the lock acquisition fails, enter the else code and execute acquire(1).

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

(3) Call the acquire(1) method, which directly enters the if judgment. In the if statement, first execute the tryAcquire(arg) method to attempt to acquire and process the re-entry lock logic again, and then execute the addWaiter() and acquireQueued(Node,int) methods if it fails.Execute the self-interrupt method selfInterrupt() to interrupt the current thread if both conditions are true.

public final void acquire(int arg) {
      if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

(4) Call the NonfairSync.tryAcquire(int) method, which calls the nonfairTryAcquire(int) method internally.

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

final boolean nonfairTryAcquire(int acquires) {
    	    // Get the current thread reference
            final Thread current = Thread.currentThread();
    	    // Get the state of the lock
            int c = getState();
    	    // If 0 means unlocked, CAS modifies the lock state to attempt to acquire the lock and successfully returns true
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }else if (current == getExclusiveOwnerThread()) {
                // Here's the logic for dealing with reentrant locks, where a reentrant puts state+1 and returns true
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
    	    // Return false if neither re-entry nor lock acquisition is possible 
            return false;
}

Call the addWaiter(Node) method.A brief description of the method is to queue the current thread in a given mode, where the Node parameter denotes the mode, Node.EXCLUSIVE denotes the mutex, and Node.SHARED denotes the shared lock.

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Attempt to join the queue quickly, call enq method to join if failed
        Node pred = tail; // Get the end of the queue element
        if (pred != null) { // If the end of the queue is not empty (that is, the queue exists)
            node.prev = pred; // Set node's preceding node not queued end node
            if (compareAndSetTail(pred, node)) { // CAS sets the end-of-queue element to node, and other threads may do the same
                pred.next = node; // Point the next of the original queue tail element to the new queue tail element node
                return node;
            }
        }
    	// If the upper tail does not exist (the queue is not initialized)
    	// Or CAS failed at the end of the setup (indicating competition, other threads set up successfully)
    	// Then call the enq(Node) method to queue the nodes
        enq(node);
        return node;
}

enq(Node) method, which is used to initialize the queue or to loop through to successfully queue nodes

private Node enq(final Node node) {
        for (;;) {
            // Get Tail Node
            Node t = tail;
            // If the trailing node is empty, try to initialize the queue
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // The following logic is the same as the fast queue entry logic in addWaiter()
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

acquireQueued(Node,int) method, which is called after an attempt to acquire a lock fails and the addWaiter() method successfully queues the Node node that packs the current thread.This method is the last entry method for each thread that has not acquired a lock and is joined to a waiting queue, in which a dead loop is used to respond to a wake-up or interrupt signal.

final boolean acquireQueued(final Node node, int arg) {
    	// Whether lock acquisition failed
        boolean failed = true;
        try {
            // Interrupt flag
            boolean interrupted = false;
            // Dead-loop response
            for (;;) {
                // This first gets the node in front of the current node, since it is usually the previous node that wakes up the latter node
                final Node p = node.predecessor();
                // If the lead node is the head node and the current thread succeeds in getting the lock
                if (p == head && tryAcquire(arg)) {
                    // Set header node to current node
                    setHead(node);
                    // Set the next node reference of the preceding node to null to help the garbage collector recycle
                    p.next = null; // help GC
                    // Setting the failure flag to false indicates successful lock acquisition
                    failed = false;
                    // Return interrupt flag for subsequent processing
                    return interrupted;
                }
                // Execute two methods after a lock acquisition failure, which are analyzed below
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;// This flag is returned to the top level for subsequent processing
            }
        } finally {
            // If the execution to this failure flag is true, follow-up work on this node is required
            if (failed)
                cancelAcquire(node);
        }
}

// This method sets the node node as the header node and empties the preceding node reference and thread reference of the node itself
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
}

The shouldParkAfterFailedAcquire() method is meant to determine if the thread should be blocked after a failure to acquire a lock, with the thread's corresponding and preceding nodes as parameters.What method can be safely blocked?

When a node's waitStatus is SIGNAL, it indicates that it has subsequent nodes and needs to wake them up when the lock is released.If that node can be safely blocked, the preceding node needs to know that it has subsequent nodes.Then this method mainly confirms whether the waitStatus of the preceding node are SIGNAL or not, otherwise it modifies the waitStatus of the preceding node to SIGNAL and waits for the next entry method to continue judging.

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	// Gets the wait state of the preceding node, which is 0 by default
        int ws = pred.waitStatus;
    	// If the state of the preceding node is SIGNAL, then this node can be safely blocked
    	// In this state, subsequent nodes are awakened when the prefix node releases the lock
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) { // If the state of the preceding node is greater than 0, then cancelled is cancelled
            
            // Then try moving forward to find the leading node with status <=0
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // Point the next node of the preceding node to this node when found
            pred.next = node;
        } else {
            // Walk here to indicate that the front node is normal, but the state of the front node is not SIGNAL
            // Then set the state of the preceding node to SIGNAL using CAS so that it knows when the preceding node releases the lock
            // It is followed by a node waiting to be notified
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
    	// Return false and wait for the next time to enter this method
        return false;
 }

The parkAndCheckInterrupt() method is designed to block threads and respond to wake-up and interrupt information.One point to explain is that the Thread.interrupted() method is a static method.

CurrtThread().isInterrupted (true) method is called internally in Thread.interrupted(), which determines if the interrupt flag of the current thread is true, returns the current thread interrupt flag if true, sets the current thread interrupt flag to false, and does nothing after returning the current thread interrupt flag if false.If a user thread calls the thread.interrupt() method only once, it returns true the first time the Thread.interrupted() method is executed, and then false no matter how many times the Thread.interrupted() method is executed.

Thus, the method parkAndCheckInterrupt() can return true only once for itself in if (shouldParkAfterFailedAcquire (p, node) & & parkAndCheckInterrupt() when the user only calls the thread.interrupt() method once.This prevents repeating statements that satisfy the if condition.

private final boolean parkAndCheckInterrupt() {
    	// Blocking thread, thread will block here after executing this method
        LockSupport.park(this);
    	// When the LockSupport.unpark(Thread) or thread.interrupt() method is called
    	// The blocked thread returns from the above method, so you can continue with the following program
        return Thread.interrupted();
}
4.2 Unlock process

(1) Call the ReetrantLock.unlock() method, which calls sync.release() internally.

public void unlock() {
        sync.release(1);
}

(2) In this case, the sync.release() method actually calls the release() method in the AQS class

public final boolean release(int arg) {
    	// Attempt to release lock
        if (tryRelease(arg)) {
            Node h = head;
            // If the header is not empty, there is a waiting queue
            // If the first condition is true, waitStatus is not zero to indicate that there are subsequent nodes to wake up
            if (h != null && h.waitStatus != 0)
                // Wake-up head node's successor nodes
                unparkSuccessor(h);
            return true;
        }
        return false;
}

(3) The Rentrant.Sync.tryRelease() method actually called by tryRelease() in this example, which mainly changes the state of the synchronizer

protected final boolean tryRelease(int releases) {
    	    // The result of subtracting the release value from the current state is assigned to c
            int c = getState() - releases;
    	    // An illegal monitor state exception is thrown if the current thread is not the thread that acquired the lock 
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
    	   // Is the tag unlocked
            boolean free = false;
    		// c==0 indicates that the lock is currently unlocked
            if (c == 0) {
                free = true;
                // Set thread to get lock to Null
                setExclusiveOwnerThread(null);
            }
    	    // Set lock status value to c
            setState(c);
    	    // Return free, if true proves the lock is completely released, and if there are successor nodes, the successor nodes can be waked up
    		// If false, it may be a case of re-entering a lock that cannot be acquired by its successor nodes
            return free;
}

(4) Call the unparkSuccessor() method under the AQS class, which handles the state of the incoming node itself and wakes up its successor nodes.

private void unparkSuccessor(Node node) {
        // Get the waitStatus of the incoming node
        int ws = node.waitStatus;
    	// If waitStatus <0 then use CAS to set node's status to 0
    	// Using CAS is concerned that other nodes are modifying the waitStatus of incoming nodes at this time
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // Get the next node of a node
        Node s = node.next;
    	// If the next node is empty or if the next node waitStatus>0 (Cancelled state)
        if (s == null || s.waitStatus > 0) {
            // Empty s first (s!=null && waitStatus>0)
            s = null;
            // Traverse from the end of the waiting queue to find an available (waitStatus<=0) node to assign to s
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
    	// If the succeeding node is not empty, wake up the thread of the succeeding node
        if (s != null)
            LockSupport.unpark(s.thread);
    }

summary

These are the principles and core methods of ReentrantLock that I personally understand. Other methods will continue to be documented in future studies.In the future, I will comb it again by writing in writing after each study, in the hope of gaining some results.I'll keep iterating over this article if I have a new understanding.

Tags: Programming Attribute

Posted on Wed, 25 Mar 2020 00:04:08 -0400 by alienmojo