AQS(AbstractQueuedSynchronizer) :抽象的队列同步器

技术解释:是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列(先进先出队列)来完成资源获取和线程排队的工作,并通过一个int类型变量表示持有锁的状态。

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH:Craing、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIF、

 

axios 检查 asq检查是什么意思_封装

 

 在并发场景中,有的线程获取不到锁,就需要进入阻塞状态,而AQS就是一个队列来管理这里线程的。以ReentrantLock为例,探讨一下AQS。

AQS(AbstractQueuedSynchronizer)这个抽象类中有一个内部类Node,用来封装阻塞线程的。
 
volatile Node prev;//指向前一个节点
 
volatile Node next;//指向后一个节点
 
private transient volatile Node head;//头结点指针
 
private transient volatile Node tail;//尾节点指针
 
private volatile int state;//表示同步状态
 
volatile Thread thread;//节点保存的线程


模仿用户去办理业务,有三个人,顾客A,顾客B,顾客C。顾客A办理业务时间长,顾客B、C都会等待。

package com.yun.AQSDemo;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        //带入一个银行办理业务的案例来模拟我们的AQS 如何进行线程的管理和通知唤醒机制
        //3个线程模拟3个来银行网点,受理窗口办理业务的顾客

        //A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " come in.");

                try {
                    TimeUnit.SECONDS.sleep(60);//模拟办理业务时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                lock.unlock();
            }
        }, "Thread A").start();

        //第2个顾客,第2个线程->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代B只能待
        //进入候客区
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " come in.");

            } finally {
                lock.unlock();
            }
        }, "Thread B").start();
        
        //第3个顾客,第3个线程->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代C只能待
        //进入候客区
        new Thread(()->{
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " come in.");

            } finally {
                lock.unlock();
            }
        }, "Thread C").start();
    }
}

ReentrantLock类中有三个内部类,一个是sync,一个是FairSync,另一个是NonfairSync。其中FairSync和NonfairSync都是继承的sync,sync继承的是AQS(AbstractQueuedSynchronizer) 。

axios 检查 asq检查是什么意思_axios 检查_02

axios 检查 asq检查是什么意思_头结点_03

axios 检查 asq检查是什么意思_axios 检查_04

执行ReentrantLock lock = new ReentrantLock();其实底层就是创建一个sync,默认创建的是非公平的锁

axios 检查 asq检查是什么意思_java_05

当顾客A先调用lock的时候,其实底层调用的是sync的lock方法。ync的lock方法是一个抽象方法,子类必须实现改方法。由于默认创建的是非公平的锁,因此这里只看NonfairSync重的lock方法。

axios 检查 asq检查是什么意思_java_06

 compareAndSetState(0, 1)方法就是一个CAS的思想,代表期望当前共享资源的占用状态是0,那么就修改占用状态为1(0表示当前共享资源没有人占用,1表示被占用),并且设置当前线程独占访问权限。

axios 检查 asq检查是什么意思_头结点_07

axios 检查 asq检查是什么意思_System_08

 第一次这个共享资源没有被线程占用,所以可以设置当前线程为工作线程,后面来的线程通过 compareAndSetState(0, 1)方法试图修改线程占用当前资源,但是占用不了,返回false,就会调用acquire()方法。因此顾客B进来会走acquire()方法。

axios 检查 asq检查是什么意思_封装_09

 这个方法里面包含了三个方法。其中当前线程还会通过tryAcquire()去尝试抢占一下资源,

axios 检查 asq检查是什么意思_java_10

 tryAcquire()方法中就只有一个抛出异常,这是一个典型的设计模式中的模板设计模式。这个方法是AQS(AbstractQueuedSynchronizer)中的方法,其子类都实现了这个方法的。就看NonfairSync子类中的实现方法。

axios 检查 asq检查是什么意思_System_11

在NonfairSync类中,该方法调用的是非公平的tryAcquire()方法。

final boolean nonfairTryAcquire(int acquires) {
            //获取当前线程,也就是顾客B
            final Thread current = Thread.currentThread();
            //获取当前资源占有状态
            //当前有资源被占用了,所以是1
            int c = getState();

            //这种情况是,刚好顾客A释放了资源,然后刚好顾客B来抢占资源
            //正好抢占到了,就会调用compareAndSetState(0, acquires)
            //也是CAS,修改当前资源类的占用状态,并且设置当前线程为资源占用线程
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //getExclusiveOwnerThread()获取当前占用资源的线程
            //判断当前进来的线程和当前占用资源的线程是不是同一个
            //这里相当于顾客A刚刚办完业务,但是又想重新办理业务
            //相当于重入锁
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //返回false,抢占失败
            return false;
        }

如果抢占失败,返回false,然后取反就是true,相当于顾客B没有抢到锁,就需要入队了,就会调用addWaiter()方法

/**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        //将当前线程,也就是顾客B封装成一个Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure

        //由于现在线程B还没有加进来,现在这个队列其实没有节点
        //所以头结点指针和尾节点指针都是null
        Node pred = tail;
        //由于不满足这个条件,现在B走的是enq这个方法
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
/**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        //这个for循环相当于while(true){}的自旋
        for (;;) {
            //由于队列中没有任何节点
            //尾指针为null,因此t==null

            //第一次循环,走的是这里,创建了第一个节点,也就是伪节点后,下一次循环就走else
            Node t = tail;
            if (t == null) { // Must initialize
                //队列中没有节点,会先初始化一个节点,相当于伪节点
                //作用就是用来占位的
                //因此线程B不是队列中的第一个节点
                if (compareAndSetHead(new Node()))
                //compareAndSetHead(new Node()这个方法也是用的CAS,希望当前位置的节点为null
                //然后更新为一个新建的节点,也就是伪节点,并且将头结点指向新建的节点
                
                //把尾节点指向头结点指向的节点
                    tail = head;
            } else {
                //第二次循环的时候,因为队列中有一个节点了
                //node封装了真正的线程B
                //先将node的前指针指向当前队列中的最后一个节点
                node.prev = t;
                //然后在通过CAS将尾节点指针指向node
                if (compareAndSetTail(t, node)) {
                    //最后再将之前尾节点的next指向node
                    t.next = node;
                    //返回t
                    return t;
                }
            }
        }
    }

 当队列中创建了这个伪节点后

axios 检查 asq检查是什么意思_java_12

把封装线程B的node节点加入队列后

axios 检查 asq检查是什么意思_java_13

此时线程B就已经入队了。 

线程C入队的时候,因为队列中已经有节点了,因此在addWaiter(Node mode) 方法中,直接会把封装线程C的节点添加进队列,不用走enq方法。

axios 检查 asq检查是什么意思_axios 检查_14

 此时B、C入队。这时候就会进入acquireQueued(final Node node, int arg)方法

/**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                //获取当前节点的前一个结点
                final Node p = node.predecessor();
                
                //判断一下该节点的前一个节点是不是头结点
                //其实就是判断一下当前节点是不是第一个有数据的节点,先进先出
                //并且尝试去抢占一下资源ryAcquire(arg)

                //当线程A不占用资源,并且调用unpark方法后
                //线程B被唤醒
                //这是一个自旋方法,线程B又会通过tryAcquire(arg)方法去尝试抢占资源
                //这次确实就能抢占到资源了
                if (p == head && tryAcquire(arg)) {
                    //setHead将封装线程B的节点设置为头结点
                    //然后再将头指针指向节点B
                    //设置节点中封装的线程为null
                    //节点的前指针为null
                    //此时当前节点就相当于是伪节点了,线程B已经去占用资源了
                    setHead(node);
                    /**
                    private void setHead(Node node) {
                        head = node;
                        node.thread = null;
                        node.prev = null;
                    }
                    */
                    //这里相当于让之前的伪节点没有任何指向,然后就会被垃圾回收器回收
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //抢占资源失败后,会调用这个方法shouldParkAfterFailedAcquire(p, node)
                //这个方法中,先获取当前节点的前一个节点的waitStatus
                //最开始waitStatus为0
                //然后判断waitStatus为不为-1,>0?
                //都不满足,然后用CAS把waitStatus从0改为-1
                //由于这是一个自旋,第二次循环的时候,waitStatus==-1,该方法返回true
                //因此会进入 parkAndCheckInterrupt()方法
                //p是当前节点的前一个节点,node是当前节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //最开始waitStatus都是0
        int ws = pred.waitStatus;
        //SIGNAL==-1
        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.
             */
            //CAS,将pred节点的waitStatus设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
/**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        //调用LockSupport.park让线程进入阻塞状态
        //此时可以认为线程B真正入队了
        //这里会一直进行阻塞,直到调用unpark方法。
        LockSupport.park(this);
         
        //返回线程中断信息
        //当调用了unpark方法后,线程B不会阻塞,因此返回false
        return Thread.interrupted();
    }

到这里之后,线程B和C才算是真正的入队了

此时线程A办理完业务,准备释放资源,调用unlock方法。unlock方法底层调用的sync.release(1)

axios 检查 asq检查是什么意思_java_15

public final boolean release(int arg) {
        //调用tryRelease释放锁,将资源的占用线程设置为null,返回true
        if (tryRelease(arg)) {
            //h就是头结点,也就是伪节点
            Node h = head;
            //因为之前已经将队列中的所有节点的waitStatus 设置为了-1
            //因此会进入unparkSuccessor(h)方法
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

release方法中调用tryRelease方法

axios 检查 asq检查是什么意思_头结点_16

tryRelease也是设计模式中的模板设计方法,跑出一个异常,让子类去实现这个方法,这次走的是ReentrantLock

axios 检查 asq检查是什么意思_System_17

protected final boolean tryRelease(int releases) {
            //当前线程的state为1,传进来的releases也是1,因此c==0
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                //设置当当前资源的占用线程为null
                setExclusiveOwnerThread(null);
            }
            //设置当前state的状态为0,表示没有线程占用这个资源
            setState(c);
            //返回true
            return free;
        }
private void unparkSuccessor(Node node) {
        //传进来的node节点就是伪节点,伪节点的waitStatus==-1
        int ws = node.waitStatus;
        //满足这个条件
        if (ws < 0)
            //用CAS将伪节点的waitStatus设置为00
            compareAndSetWaitStatus(node, ws, 0);

        //s节点就是第一个有内容的节点,也就是B节点
        Node s = node.next;
        
        //B节点不为null,并且B节点的waitStatus为-1
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //B节点不为null
        if (s != null)
            //调用unpark唤醒B节点
            LockSupport.unpark(s.thread);
    }

当线程A释放资源,会调用unpark方法,之前线程B和线程C都调用了park方法进行阻塞,线程A调用了unpark方法后就会唤醒线程B。因此在parkAndCheckInterrupt方法中会返回false。

axios 检查 asq检查是什么意思_axios 检查_18

parkAndCheckInterrupt返回false之后,因为acquireQueued方法是自旋,会再一次进行循环,这次线程B就会通过tryAcquire方法抢占到资源,然后再将节点B设置为头结点,里面的线程设为null,并且取消与之前的伪节点的联系,让节点B成为新的伪节点,之前的伪节点被GC回收。

=========================================================================

其他:

相当于在非公平锁下,新加进来的现在在入队之前最少都要尝试抢占4次资源

第一次是在刚调用sync中的lock方法锁定时,会尝试抢一次

axios 检查 asq检查是什么意思_java_19

 第二次是在acquire方法中的tryAcquire方法中尝试抢占一次

axios 检查 asq检查是什么意思_System_20

 第三、四次会在acquireQueued方法中抢占,因为acquireQueued是自旋,第一次循环的时候会抢占一次,并且设置当前节点的前一个节点的waitStatus==-1,第二次循环的时候再抢占一次,然后在调用park方法将当前线程进行阻塞

axios 检查 asq检查是什么意思_头结点_21