One article allows you to thoroughly understand AQS (easy-to-understand AQS)

One article allows you to thoroughly understand AQS (easy-to-understand AQS)

1. What is AQS

  • AQS is a framework for building locks and synchronizers. Using AQS can easily and efficiently construct a large number of widely used synchronizers, such as the ReentrantLock, Semaphore we mentioned, and others such as ReentrantReadWriteLock, SynchronousQueue, FutureTask, etc. It is based on AQS. Of course, we can also use AQS to construct a synchronizer that meets our own needs very easily.

2. Prerequisite knowledge

3. The core idea of ​​AQS

  • The core idea of ​​AQS is that if the requested shared resource is idle, the thread currently requesting the resource is set as a valid worker thread, and the shared resource is set to the locked state. If the requested shared resource is occupied, then a set of mechanisms for thread blocking and waiting and lock allocation when awakened are needed . This mechanism AQS is implemented using CLH queue lock, which means that threads that cannot temporarily obtain the lock are added to the queue. The CLH (Craig, Landin, and Hagersten) queue is a virtual two-way queue (a virtual two-way queue does not have a queue instance, only the association between nodes). AQS encapsulates each thread requesting shared resources into a node (Node) of a CLH lock queue to implement lock allocation. AQS uses an int member variable to represent the synchronization status, and completes the queuing work of resource acquisition threads through the built-in FIFO queue. AQS uses CAS to perform atomic operations on the synchronization state to modify its value. (Figure 1 is a node relationship diagram)
private volatile int state;//共享变量,使用volatile修饰保证线程可见性

Insert image description here

4. AQS case analysis

上面讲述的原理还是太抽象了,那我我们上示例,结合案例来分析AQS 同步器的原理。以ReentrantLock使用方式为例。
代码如下:
public class AQSDemo {
    
    
    private static int num;


    public static void main(String[] args) {
    
    

        ReentrantLock lock = new ReentrantLock();



        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                lock.lock();
                try {
    
    
                        Thread.sleep(1000);
                        num += 1000;
                    System.out.println("A 线程执行了1秒,num = "+ num);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                finally {
    
    
                    lock.unlock();
                }
            }
        },"A").start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                lock.lock();
                try {
    
    
                    Thread.sleep(500);
                    num += 500;
                    System.out.println("B 线程执行了0.5秒,num = "+ num);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                finally {
    
    
                    lock.unlock();
                }
            }
        },"B").start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                lock.lock();
                try {
    
    
                    Thread.sleep(100);
                    num += 100;
                    System.out.println("C 线程执行了0.1秒,num = "+ num);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                finally {
    
    
                    lock.unlock();
                }
            }
        },"C").start();
    }
}


A certain result of execution! This code is super simple, but the execution results may be different. You can experiment by yourself.
Result one
Insert image description here
Insert image description here
Comparing the three results, you will find that no matter what the result is, the final value of num is always 1600, which shows that our locking is successful.

5. AQS source code analysis

  • The method of use is very simple, just use the thread to manipulate the resource class. There are two main methods: lock() and unlock(). Let’s go deep into the code to understand. I added comments based on the source code, and I hope everyone will follow suit to debug the source code. It's actually very simple.

5.1 AQS data structure

AQS 主要有三大属性分别是 head ,tail, state,其中state 表示同步状态,head为等待队列的头结点,tail 指向队列的尾节点。
    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;
 还需要再去了解 Node的数据结构,
在这里插入代码片
class Node{
    
    
  //节点等待状态
  volatile int waitStatus;
  // 双向链表当前节点前节点
  volatile Node prev;
  // 下一个节点
  volatile Node next;
  // 当前节点存放的线程
  volatile Thread thread;
  // condition条件等待的下一个节点
  Node nextWaiter;
}

waitStatus has only a few specific constants, and the corresponding values ​​are explained as follows:
Insert image description here
In this source code explanation, we take the unfair lock of ReentranLock as an example. The methods we mainly focus on are lock() and unlock().

5.2 lock source code analysis

First, let’s take a look at the source code of the lock() method and directly enter the lock method of unfair lock:

final void lock() {
    
    
            //1、判断当前state 状态, 没有锁则当前线程抢占锁
            if (compareAndSetState(0, 1))
                // 独占锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 2、锁被人占了,尝试获取锁,关键方法了
                acquire(1);
        }

Enter the acquire() method of AQS:

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

total-point-total

  • The lock method mainly uses tryAquire() to try to acquire the lock, addWaiter(Node.EXCLUSIVE) to join the waiting queue, and acquireQueued(node,arg) to wait for the queue to try to acquire the lock. The schematic diagram is as follows:
    The overall process of lock method
5.2.1 tryAquire method source code
  • Since it is an unfair lock, we want to grab the lock as soon as we come in. No matter what, we just try to see if we can grab it. If we can't grab it, we will enter the queue.
  final boolean nonfairTryAcquire(int acquires) {
    
    
            //1、获取当前线程
            final Thread current = Thread.currentThread();
            // 2、获取当前锁的状态,0 表示没有被线程占有,>0 表示锁被别的线程占有
            int c = getState();
            // 3、如果锁没有被线程占有
            if (c == 0) {
    
    
                 // 3.1、 使用CAS去获取锁,   为什么用case呢,防止在获取c之后 c的状态被修改了,保证原子性
                if (compareAndSetState(0, acquires)) {
    
    
                    // 3.2、设置独占锁
                    setExclusiveOwnerThread(current);
                    // 3.3、当前线程获取到锁后,直接发挥true
                    return true;
                }
            }
            // 4、判断当前占有锁的线程是不是自己
            else if (current == getExclusiveOwnerThread()) {
    
    
                // 4.1 可重入锁,加+1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                 // 4.2 设置锁的状态
                setState(nextc);
                return true;
            }
            return false;
        }

5.2.2 Analysis of addWaiter() method

  • private Node addWaiter(Node mode), when the current thread has no lock, it enters the CLH queue.
 private Node addWaiter(Node mode) {
    
    
 		// 1、初始化当前线程节点,虚拟节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 2、获取尾节点,初始进入节点是null
        Node pred = tail;
        // 3、如果尾节点不为null,怎将当前线程节点放到队列尾部,并返回当前节点
        if (pred != null) {
    
    
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
    
    
                pred.next = node;
                return node;
            }
        }
        // 如果尾节点为null(其实是链表没有初始化),怎进入enq方法
        enq(node);
        return node;
    }
    
   // 这个方法可以认为是初始化链表
   private Node enq(final Node node) {
    
    
   		// 1、入队 : 为什么要用循环呢?  
        for (;;) {
    
    
           // 获取尾节点
            Node t = tail;
           // 2、尾节点为null
            if (t == null) {
    
     // Must initialize
               // 2.1 初始话头结点和尾节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 
            // 3、将当前节点加入链表尾部
            else {
    
    
                node.prev = t;
                if (compareAndSetTail(t, node)) {
    
    
                    t.next = node;
                    return t;
                }
            }
        }
    }

Does anyone want to understand why enq needs to use for(;;)? At first glance, it only takes 2 cycles at most! Here comes the answer. This is indeed the case for single threads, but for multi-threads, it is possible that after the completion of the second part, it will be executed into the linked list by other threads first. At this time, it is discovered after the third step cas What should I do if it fails? You can only loop again and try to join the linked list until you succeed.

5.2.3 Detailed explanation of acquireQueued() method

  • addWaiter method We have placed threads that have not acquired the lock in the waiting list, but these threads are not in a waiting state. The function of acquireQueued is to set the thread to a waiting state.
 final boolean acquireQueued(final Node node, int arg) {
    
    
         // 失败标识
        boolean failed = true;
        try {
    
    
            // 中断标识
            boolean interrupted = false;
            for (;;) {
    
    
                // 获取当前节点的前一个节点
                final Node p = node.predecessor();
                // 1、如果前节点是头结点,那么去尝试获取锁
                if (p == head && tryAcquire(arg)) {
    
    
                    // 重置头结点
                    setHead(node);
                    p.next = null; // help GC
                    // 获得锁
                    failed = false;
                    // 返回false,节点获得锁,,,然后现在只有自己一个线程了这个时候就会自己唤醒自己
                    // 使用的是acquire中的selfInterrupt(); 
                    return interrupted;
                }
                // 2、如果线程没有获得锁,且节点waitStatus=0,shouldParkAfterFailedAcquire并将节点的waitStatus赋值为-1
                //parkAndCheckInterrupt将线程park,进入等待模式,
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
    
    
            if (failed)
                cancelAcquire(node);
        }
    }

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;
    }
  • Okay, the explanation of this source code is over. Are you still confused? You have to admit that this code is too elegant. As expected of God!

Let me put it together for everyone in vernacular! We will explain the unfair lock of reentrantLock in combination with our case 4.
When thread A reaches the lock() method, it obtains the lock through compareAndSetState(0,1) and obtains the exclusive lock. When the B and C threads compete for the lock, they run to acquire(1), and the C thread runs tryAcquire(1), then runs the nonfairTryAcquire(1) method, does not acquire the lock, and finally returns false, runs addWaiter(), and runs enq( node), initialize the head node, and C enters the queue; then enter the acquireQueued(node,1) method, initialize waitStatus= -1, spin and park() to wait.
Then the B thread starts to grab the lock. The B thread runs tryAcquire(1) and runs the nonfairTryAcquire(1) method. If the lock is not obtained, it returns false. It runs addWaiter() and is directly added to the end of the queue. At the same time, B enters the queue; after entering acquireQueued( node, 1) method, initialize waitStatus= -1, spin and park() to wait.

AQS core method flow chart

5.3 unlock source code analysis

unlock releases the lock. The main use is LockSupport

  public final boolean release(int arg) {
    
    
         // 如果成功释放独占锁,
        if (tryRelease(arg)) {
    
    
            Node h = head;
            // 如果头结点不为null,且后续有入队结点
            if (h != null && h.waitStatus != 0)
                //释放当前线程,并激活等待队里的第一个有效节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 如果释放锁着返回true,否者返回false
    // 并且将sate 设置为0
 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;
        }


  private void unparkSuccessor(Node node) {
    
    
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            // 重置头结点的状态waitStatus
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
         // 获取头结点的下一个节点
        Node s = node.next;
        // s.waitStatus > 0 为取消状态 ,结点为空且被取消
        if (s == null || s.waitStatus > 0) {
    
    
            s = null;
            // 获取队列里没有cancel的最前面的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果节点s不为null,则获得锁
        if (s != null)
            LockSupport.unpark(s.thread);
    }

Releasing the lock is still very simple.

Summarize

The best way to read this source code is to follow the code step by step with examples and write each step on paper. After trying it once or twice, you will have a very clear understanding.

Please give me more opinions. Before I wrote it, I was confident that I could write something that everyone could understand. But after I finished writing it, I felt like shit.

Guess you like

Origin blog.csdn.net/u010445301/article/details/125590758