AQS: full name AbstractQueuedSynchronizer (abstract queue synchronizer), it is an abstract class, main effect is as follows

  1. A framework for implementing locks is provided based on FIFO queues and state bits
  2. Exclusive and shared locks are supported
  3. ConditionObject is internally defined, which is an implementation class of the Condition interface that is essential to using Condition’s related functions

ReentrantLock implements its own locking mechanism based on AQS

ReentrantLock source code parsing

Let’s start by looking at the inheritance relationships between the ReentrantLock classes

Sync, NonfairSync, and FairSync are all internal classes of ReentrantLock

The Sync inner class inherits AQS and overwrites the tryRelease method, while the Sync subclasses NonfairSync (unfair) and FairSync (fair) overwrite the tryAcquire method, respectively

// You can specify fair or unfair locks when creating a ReentrantLock object
// The default constructor creates unfair locks
public ReentrantLock(a) {
  sync = new NonfairSync();
}

// Enter true to create a fair lock, false to create an unfair lock
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}
Copy the code
static final class Node {
       
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        // This object was cancelled due to timeout or interruption
        static final int CANCELLED =  1;
  	// When a Node joins a queue, the state of the previous Node is set to SIGNAL, indicating that the Node needs to be woken up
        static final int SIGNAL = -1;
  	// This object is currently in the condition wait queue
        static final int CONDITION = -2;
       	// TODO: I don't know what this status bit does
        static final int PROPAGATE = -3;
 
  	// The status of this object in the synchronization queue. The initial value is 0
        volatile int waitStatus;
        // The precursor node
        volatile Node prev;
        // Subsequent nodes
        volatile Node next;
        // The thread of the node
        volatile Thread thread;
        // points to the next node in CONDITION
        Node nextWaiter;
}
Copy the code

Fair lock lock

From the fair lock lock process to explain the source code

final void lock(a) {
  	acquire(1);
}
Copy the code
public final void acquire(int arg) {
  // There are two judgments here
  // tryAcquire: Return true to acquire the lock successfully, false to acquire the lock failed. The method that succeeded in obtaining the lock is returned. You do not need to go down. Otherwise, you need to go to the queue operation
  AcquireQueued (addWaiter(node.exclusive), arg) : The thread fails to obtain the lock and joins the queue
  if(! tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }Copy the code

A hook method in AQS that subclasses must implement or throw an exception

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
Copy the code
	/ / fair lock
	static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
      	
      	// Override the template method in AQS
        protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                // Get the value of state
                int c = getState();
          	// State is 0, indicating that the node is in the initial state
                if (c == 0) {
                    Hasqueuedboomreturns false to indicate that there are no waiting threads in the wait queue
                    // compareAndSetState The CAS state is successfully changed from 0 to 1. The CAS state is successfully locked, ensuring that only one thread can obtain the lock at the same time
                    // Enter the team when the scramble fails
                    if(! hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                        // Set an exclusive thread
                        setExclusiveOwnerThread(current);
                        return true; }}// ReentrantLock is a ReentrantLock, which means the same thread can be locked repeatedly
                // Check whether it is the same thread
                else if (current == getExclusiveOwnerThread()) {
                    // state + 1
                    int nextc = c + acquires;
                    // Repeat the lock to reach the upper limit
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                // false indicates that the thread did not acquire the lock
                return false; }}}Copy the code
// Return true to indicate that there are already waiting queues in the blocking queue
// Return false to indicate that there are no waiting queues in the blocking queue
public final boolean hasQueuedPredecessors(a) {
    Node t = tail; 
    Node h = head;
    Node s;
    returnh ! = t && ((s = h.next) ==null|| s.thread ! = Thread.currentThread()); }Copy the code

h ! = t &&((s = h.next) == null || s.thread ! = Thread.currentThread());

Whether this judgment is true depends on three conditions:

  1. The queue is not initialized
  2. The queue is initialized, but there is only one node in the queue
  3. The queue has been initialized and there is more than one node in the queue

If the queue is not initialized, tail and head are null, h! If t is not true, return false

Tail and head both point to the same node. H! If t is not true, return false

Third case: there are multiple nodes in the queue. = t (true), h.next is not null (false), s.read! CurrentThread () returns false if the Thread currently acquiring the lock is the same as the Thread in the node, true if it is not

// Head node in AQS queue
private transient volatile Node head;
// Last node in AQS queue
private transient volatile Node tail;


private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // pred Set the value to tail
    Node pred = tail;
    // The FIFO queue is initialized and the FIFO queue is not initialized
    // If pred is null and the queue is not initialized, use the enq(node) method
    // If the queue is already initialized, it is equivalent to adding nodes behind the queue
    if(pred ! =null) {
      node.prev = pred;
      // If the CAS fails, enq also has a CAS to the end of the queue, and it is for an infinite loop
      if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
      }
    }
    enq(node);
    return node;
}

// This method can be seen in the following figure
private Node enq(final Node node) {
    / / death cycle
    for (;;) {
      Node t = tail;
      // tail is null when initialized, so the first loop goes if first
      if (t == null) { // Must initialize
        // Set a sentinel node (sentinel node is an empty node)
        if (compareAndSetHead(new Node()))
          // Both the head and tail nodes point to the sentinel node
          tail = head;
      // The second loop goes else
      } else {
        node.prev = t;
        if (compareAndSetTail(t, node)) {
          // Add node after queue
          t.next = node;
          returnt; }}}}Copy the code

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
      // Indicates whether the thread is interrupted while waiting
      boolean interrupted = false;
      for (;;) {
        // Get the precursor node of this node
        final Node p = node.predecessor();
        // Here are two judgments
        // p == head: if the first node is the head node, the current thread is the first one waiting.
        // tryAcquire(ARG) : If the current thread is the first thread waiting, try to acquire the lock again, which is equivalent to a spin operation
        // If the spin lock succeeds, the current node becomes the head node
        // If the current thread is not the first thread waiting, no spin is performed
        if (p == head && tryAcquire(arg)) {
          setHead(node);
          p.next = null; // help GC
          failed = false;
          // In theory this is the only exit from the for loop
          return interrupted;
        }
        // The current thread is not the first thread to wait and park blocks
        / / shouldParkAfterFailedAcquire return true will perform parkAndCheckInterrupt this method of the nodes in the park
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
          // The thread has been interrupted
          interrupted = true; }}finally {
      if(failed) cancelAcquire(node); }}Copy the code
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  	// Get the status of the precursor node
        int ws = pred.waitStatus;
  	// The status bit of the precursor node is already -1, no processing is done
        if (ws == Node.SIGNAL)
            return true;
  	// If the status bit of the precursor node is >0, it indicates that the precursor node may be interrupted or timed out. The cancelled node is discarded here
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // Set the state of the precursor node to -1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
 }
Copy the code

The main purpose of this method is to ensure that the status of the precursor node of the current node is SIGNAL

P is the precursor node of node. Whether a node needs to be blocked is determined by the status of the precursor node of this node. There are three cases in this method

  1. The status of the precursor node issingalIs displayed, indicating that the precursor node is still waiting and the current node must continue to be blocked. Returns true
  2. If the precursor node is greater than 0, it indicates that the precursor node may be interrupted or timed out, and the node in cancelled state should be discarded
  3. The precursor node is in another state (0,PROPAGATE), which means that the current node needs to retry the attempt to acquire the lock. Returns false, enclosing a for loop that does one more spin lock
private final boolean parkAndCheckInterrupt(a) {
    // Block the thread
    LockSupport.park(this);
    // Checks whether the thread has been interrupted, and returns true if it has
    return Thread.interrupted();
}
Copy the code

Locking flow chart

Fair and Unfair Locks Unlock

Interpret the source code from the lock release process

public void unlock(a) {
  	sync.release(1);
}
Copy the code
public final boolean release(int arg) {
    if (tryRelease(arg)) {
      Node h = head;
      if(h ! =null&& h.waitStatus ! =0)
        // Wake up the next node in the wait queue
        unparkSuccessor(h);
      return true;
    }
    return false;
}
Copy the code
protected final boolean tryRelease(int releases) {
    // ReentrantLock is reentrant and will be state-1
    int c = getState() - releases;
    // Check whether the current thread is in an exclusive lock
    if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
    boolean free = false;
    // a c value of 0 means that no thread holds the lock
    if (c == 0) {
      free = true;
      setExclusiveOwnerThread(null);
    }
    setState(c);
    // If free is true, no thread holds the lock
    // If free is false, there are threads holding the lock
    return free;
}
Copy the code
private void unparkSuccessor(Node node) {
		
    int ws = node.waitStatus;
    if (ws < 0)
      compareAndSetWaitStatus(node, ws, 0);

    // Get the successor node of the current node (the current node is the head node)
    Node s = node.next;
    // If the subsequent node is empty, or cancelled
    // where s==null does not mean that S is tail. At this point, a new node may join the queue and the cas is completed but the next pointer is not updated
    if (s == null || s.waitStatus > 0) {
      s = null;
      // Start at tail and walk forward to find the node whose status is not CANCELLED
      In addWaiter and enq, joining the queue is not an atomic operation (3 steps: 1 set prev pointer,2 set tail pointer,3 set next // pointer).
      // So if you go back and forth, you might miss the node because the next pointer might be null.
      for(Node t = tail; t ! =null&& t ! = node; t = t.prev)if (t.waitStatus <= 0)
          s = t;
    }
    // Wake up the blocking node
    if(s ! =null)
      LockSupport.unpark(s.thread);
}
Copy the code

An unfair lock

abstract static class Sync extends AbstractQueuedSynchronizer {
        
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
              	// The difference with fair locking is that the queue does not determine whether there is a waiting thread, directly lock
                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; }}Copy the code

conclusion

For ReentrantLock, the execution logic is as follows:

  1. An attempt is made to acquire the lock of an object. If not, unfair locks are treated differently than fair locks

    • The fair lock goes straight to the end of the blocking queue,

    • In an unfair lock, CAS is performed first to obtain the lock. If the lock fails to be obtained, the system enters the end of the blocking queue in the same way as that in a fair lock

  2. When it does, it determines whether it has been retrieved once before to decide whether to reenter

  3. When the lock is released, the state member variable is decrement by 1. If state is not zero after decrement by 1, the release operation is completed. If the state value is 0 after the operation of minus 1, then the unpark method of LockSupport is called to wake up the first subsequent thread in the waiting queue behind the thread and wake it up so that it can acquire the lock of the object (the processing logic of fair lock and unfair lock is the same).

The Debug code

Start two threads, one thread to acquire the lock, you can see the other thread blocking process

public class ReentrantLockTest {

    private final Lock reentrantLock = new ReentrantLock(true);

    private void method(a) {

        try {
            reentrantLock.lock();

            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("method");
        } finally{ reentrantLock.unlock(); }}public static void main(String[] args) {

        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();

        Thread t1 = new Thread(reentrantLockTest::method);
        Thread t2 = new Thread(reentrantLockTest::method);
        t1.setName("t1");
        t2.setName("t2"); t1.start(); t2.start(); }}Copy the code