In-depth analysis of AQS principle under ReentrantLock source code

Introduction to AQS

The full name of AQS is AbstractQueuedSynchronizer , which is an abstract class that internally implements a FIFO two-way linked list. Each node in the linked list has a pointer to the previous node and a pointer to the next node, so AQS can quickly access the predecessor and the next node from any node. Subsequently, each node is bound to a thread. When the thread fails to compete for the lock, it will be added to the tail of the queue and wait to be released. When the lock is released, the thread at the head node of the queue will be released to compete for the lock.

ReentrantLock is the implementation class of the Lock interface. It is a commonly used object synchronization lock, and it is a reentrant lock. A reentrant lock means that after a thread acquires a lock, it does not need to be blocked to acquire the lock again, but is directly associated with a counter. Increase the number of reentries, for details, please refer to this article

ReentrantLock encapsulates an internal class Sync and inherits the AbstractQueuedSynchronizer abstract class. The principle of ReentrantLock locking is based on Sync. Let's take a look at the source code

// 加锁
public void lock() {
    
    
	sync.lock();
}
// 解锁
public void unlock() {
    
    
	sync.release(1);
}

This article analyzes the implementation principle of AQS from the source code of ReentrantLock

ReentrantLock source code

ReentrantLock also encapsulates the static internal classes of two subclasses of Sync, namely NonfairSync and FairSync. NonfairSync literally means that it is an unfair lock, and FairSync is a fair lock. This article mainly analyzes NonfairSync

  • NonfairSync.lock
final void lock() {
    
    
	//通过cas操作来修改state状态,表示争抢锁的操作
    if (compareAndSetState(0, 1))	
    	// 设置当前获取到锁的线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);		// 未获取到锁的线程再次尝试获取锁
}

This code briefly explains

  • First go to CAS to seize the lock, if the lock is successfully seized
  • Save the current thread that successfully acquired the lock
  • Failed to seize the lock, call acquire to walk through the lock competition logic

Look at compareAndSetState again

protected final boolean compareAndSetState(int expect, int update) {
    
    
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSetState uses the CAS method to set the state to 1. The CAS method is to compare whether the content to be modified is the same as the expected value (the expected value is the value before modification, which may be modified by other threads before modification, so first judge whether it is consistent with the expected value) The expected value is the same), if the modification is the same, it will return true, if it is not the same, it will return false if the modification fails, this series of operations is an atomic operation. The principle of CAS can be referred to here

AbstractQueuedSynchronizer encapsulates some methods of setting variable values ​​based on CAS. Let's take a look at the source code:

	private static final Unsafe unsafe = Unsafe.getUnsafe();
	private static final long stateOffset;
	private static final long headOffset;
	private static final long tailOffset;
	private static final long waitStatusOffset;
	private static final long nextOffset;
	
	static {
    
    
	   try {
    
    
	       stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
	       headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
	       tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
	       waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
	       nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
	   } catch (Exception ex) {
    
     throw new Error(ex); }
	}
	
	private final boolean compareAndSetHead(Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, headOffset, null, update);
	}
	
	private final boolean compareAndSetTail(Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
	}
	
	private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
    
    
	   return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
	}
	
	private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    
    
	   return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
	}

Unsafe's CAS method

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

The core of this part is to call the method of the Unsafe class. About the Unsafe class, I introduced it in the previous article on the analysis of the CAS principle. It provides a (native) local method for operating memory based on the cpu instruction set. Each method has four input parameters. , the second input parameter is the address in memory of several member variables of AbstractQueuedSynchronizer. The general implementation process of these methods is to first obtain the memory value of the variable through the variable address, and then compare it with the expected value. If the value is the same, the memory value is updated as update Value returns true, otherwise it does nothing and returns false.

Going back to the lock method, the process of acquiring a lock can be understood as:

  • When state=0, it means no lock state
  • When state>0, it means that a thread has acquired the lock. Any thread that modifies it to 1 through CAS successfully acquires the lock. However, because ReentrantLock allows reentrant, when the same thread acquires the synchronization lock multiple times, the state It will increase, for example, reentry 5 times, then state=5. When releasing the lock, it also needs to be released 5 times until the state=0 other threads are eligible to acquire the lock

acquire

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

The main logic of this method is

  • Try to acquire an exclusive lock through tryAcquire, return true if successful, return false if failed
  • If tryAcquire fails, the current thread will be encapsulated into a Node and added to the end of the AQS queue through the addWaiter method
  • acquireQueued, takes Node as a parameter, and tries to acquire the lock by spinning.

Node
encapsulates a Node internal class inside the AQS queue to save threads. Node is a data structure of a FIFO doubly linked list. The characteristic of this structure is that each data structure has two pointers, pointing to the successor node of the node respectively. and precursor nodes. Each Node is actually encapsulated by a thread. When the thread fails to compete for the lock, it will be encapsulated as a Node and added to the ASQ queue.

Node source code:

static final class Node {
    
    
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1; 
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;	// 等待状态标识
        volatile Node prev;		// 前节点
        volatile Node next;		// 后节点
        volatile Thread thread;	// 竞争锁的线程
        // 存储在condition队列中的后继节点
        Node nextWaiter;
		// 是否为共享锁
        final boolean isShared() {
    
    
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {
    
    
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {
    
        // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {
    
         // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) {
    
     // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

thread, prev, and next are the current thread, the previous node, and the next node respectively, so a Node queue can start from any node and traverse from front to back to the head and tail of the queue

NonfairSync.tryAcquire method

protected final boolean tryAcquire(int acquires) {
    
    
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    
    
	// 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state值,即获取锁状态
    int c = getState();
    // state等于0表示无锁状态直接获取锁并返回
    if (c == 0) {
    
    
        if (compareAndSetState(0, acquires)) {
    
    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果是已拿到锁的线程再次获取锁则state加上1,代表该锁被重入一次
    else if (current == getExclusiveOwnerThread()) {
    
    
        int nextc = c + acquires;  锁状态 + 1 表示重入次数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 未获取到锁返回true
    return false;
}

The tryAcquire method is to try to acquire an exclusive lock. After the CAS fails to acquire the lock in the first step, it acquires the lock again before adding it to the AQS queue. Because it is an unfair lock, the thread that may have acquired the lock before being added to the AQS queue releases the lock at this time. At this time, the thread competing for the lock behind can happen to preempt the lock before adding the AQS queue, so that sometimes the thread competing for the lock behind can preempt the lock faster.

Under the fair lock, each thread that fails to acquire the lock in CAS will be added to the AQS queue in strict accordance with the principle of first-in-first-out, so that the threads competing for the lock will seize the lock in strict order

AbstractQueuedSynchronizer.addWaiter method

private Node addWaiter(Node mode) {
    
    
// 将线程封装到Node中,mode为EXCLUSIVE即为独占锁
    Node node = new Node(Thread.currentThread(), mode);
    // tail是AQS的一个属性,代表队列尾节点
    Node pred = tail;
    if (pred != null) {
    
    		// tail不为空的情况,说明队列已经被初始化即有节点数据
        node.prev = pred;	// 当前线程Node的前节点指向AQS队列尾节点
        if (compareAndSetTail(pred, node)) {
    
     	// 通过CAS方式将Node添加到AQS队列尾部
            pred.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
            return node;
        }
    }
    enq(node);	// CAS失败或AQS队列没有节点数据则进入eq方法
    return node;
}

The whole process of the addWaiter method is to encapsulate the thread into a Node, and add it to the end of the AQS queue through CAS

AbstractQueuedSynchronizer.enq method

private Node enq(final Node node) {
    
    
// 进入无限for循环,即自旋
    for (;;) {
    
    
        Node t = tail;	// 获取AQS队列尾节点
        if (t == null) {
    
     // tail为空表示AQS队列没有数据需要进行初始化
       		// 通过CAS方式初始化AQS队列即创建一个空Node作为队列头部
            if (compareAndSetHead(new Node()))	
                tail = head;  // CAS成功此时AQS队列只有一个节点,因此队列头尾都是该节点
        } else {
    
    	// 如果此时AQS队列已被初始化则将Node添加到队列尾部
            node.prev = t;	// Node节点指向AQS队列尾节点
            if (compareAndSetTail(t, node)) {
    
    	// 通过CAS方式将Node添加到AQS队列尾部
                t.next = node;	// CAS成功则原来的AQS尾节点的后节点指向当前线程Node
                return t;
            }
        }
    }
}

The whole process of the eq method is to initialize the AQS queue. First, make sure that the queue has no node data, and then add the current thread Node to the head of the queue through CAS. At this time, the queue has only one node, so point the previous node of Node to itself. And set the tail of the queue as Node by means of CAS, and point the rear node of the tail to Node.

At this time, if CAS fails, other threads have completed the initialization, then the Node is added to the tail of the queue through CAS, and then the node behind the tail node of the queue is pointed to the Node. If CAS fails again at this time, this series of The operation is to spin until the tail of the queue is added successfully, then the spin ends.

AbstractQueuedSynchronizer.acquireQueued method

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;	// 失败标识
    try {
    
    
        boolean interrupted = false;	// 线程终端标识
        for (;;) {
    
    
            final Node p = node.predecessor();	// 获取当前线程Node的前节点
            // 如果Node的前节点为AQS头部head即Node处于队列最前端,每次只有队列最前端的Node才能后去抢占锁,直到抢占成功
            if (p == head && tryAcquire(arg)) {
    
    	
                setHead(node);	// 线程抢占锁成功则将将该线程Node从队列中移除
                p.next = null; 	// 线程Node被设为head,原来的head后节点设为null使其能被GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}
private void setHead(Node node) {
    
    
    head = node;
    node.thread = null;
    node.prev = null;
}

The process of the entire acquireQueued method is roughly as follows:

  1. Get the prev of the current thread Node, if prev is the head node, then it will compete for the lock, call the tryAcquire method to seize the lock
  2. If the Node successfully seizes the lock, set the Node as the head, and remove the original initialization head node
  3. If the acquisition of the lock fails, it is determined whether the thread needs to be suspended according to waitStatus. After the thread is suspended, cancel the operation of acquiring the lock through cancelAcquire

Guess you like

Origin blog.csdn.net/weixin_44947701/article/details/125191641