AQS(一):互斥锁的加锁和锁的释放流程

一、什么是AQS

        AQS就是类 AbstractQueuedSynchronizer 的简称,AQS是juc包下的一个基类,juc包

       下很多工具类都是基于AQS实现了部分内容,如:ReentrantLock、ThreadPoolExecutor

        、阻塞队列、CountDownLatch、Semaphore、CyclicBarrier等等都是基于AQS实现的。

        首先AQS提供了一个由 volatile 修饰的,且采用CAS更新的int类型变量 state

        其次AQS维护了一个双向链表(队列),有head 和 tail,且每一个节点都是Node 对象

          1)AQS属性结构如下:

               

        

        2)AQS属性字段:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    //双向链表头节点
    private transient volatile Node head;
    //双向链表尾节点
    private transient volatile Node tail;

    //状态
    private volatile int state;
    
    //内部类 Node,表示链表的每一个节点
    static final class Node {
        /* */
        static final Node SHARED = new Node();
        /**  */
        static final Node EXCLUSIVE = null;
        
        //下边是aqs的state的值(状态)常量

        /**唯一大于0的节点,表示节点可以取消  */
        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;

        /**
         * 
         */
        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;
        }
    
}

二、AQS原理之互斥锁的加锁和锁的释放

1、互斥锁的加锁的过程

1.1、acquire() 方法

        acquire方法是AQS中互斥锁 加锁的入口,在该方法中当前线程通过 tryAcquire() 获取同

       步状态(获取锁)失败后,需要将线包装成Node 节点,然后放入AQS队列中

       acquire 是一个模版方法,子类只需要实现自己的 tryAcquire(),如此设计的原因是AQS

      不知道子类加锁的行为是如何设计的

     acquire() 方法 结构如下

        

1.2、addWaiter() 方法

         该方法的作用是将没有抢到 锁资源的当前线程放到AQS队列中去排队;

         该方法采用 “全路径+优化前置”  的方式实现快速入队列。

         参数:

                 mode:表示节点模式,值有2种:EXCLUSIVE =独占的节点(互斥锁),

                             SHARED =共享的节点(共享锁)

               addWaiter() 结构如下:

               

1.3、end(Node node) 方法

         该方法作用是把 当前线程节点添加到AQS队列中,每次都把新创建的Node节点添加到

         队尾;

         end() 方法结构如下:

private Node enq(final Node node) {
        /**
         * 向队列中添加节点,每次添加节点都是把新节点添加到尾部
         * todo 问题:添加队列时为什么要使用循环?
         *      并发条件下,线程A,B 同时调用 enq() 方法,当B线程执行到 compareAndSetTail(t, node)之前,
         *      线程 A 正好执行完 t = tail; 这时队列尾节点 tail 还没发生变化,当
         *      B 线程 执行完 compareAndSetTail(t, node) 后,tail 的值被改变了,这时 线程 A 再执行
         *      compareAndSetTail(t, node) 时,就会失败,需要重新获取队列尾tail 的值,所以这里使用循环
         *      可以保证enq() 方法的原子性
         */
        for (;;) {
            Node t = tail;
            //若当前尾节点为null,则说明当前队列为null,所以先初始化队列
            if (t == null) { // Must initialize  懒加载时,head和tail分别代表AQS的头和尾,
                //使用CAS实现原子化初始化操作,用一个空节点来初始化,此时head和tail都指向这个空节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {//当前队列不为null
                //将当前节点的前置指向tail
                node.prev = t;
                /**
                 * compareAndSetTail(t, node): 这一步将CAS更新尾节点,设置尾节点tail 指向 node
                 */
                if (compareAndSetTail(t, node)) {
                    //添加节点
                    t.next = node;
                    return t;
                }
                //compareAndSetTail(t, node) 若执行失败,则进入下一次循环重新获取 tail 的值
            }
        }
    }

1.4、acquireQueued(final Node node, int arg) 方法

        该方法的作用是:线程节点node已经存在于AQS队列中,调用该方法判断 是否将node关联

        的线程阻塞

        注意一个情况:当加入node加入阻塞队列后,若当前锁是无锁时,该怎么办?

                               这种情况,那么当前线程一定会尝试获取锁

        在该方法中可以发现,AQS队列的头节点是获取锁的节点,即活动的节点

       acquireQueued方法结构如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            //标记线程是否中断,中断标志
            boolean interrupted = false;
            //死循环,这是一个 “自旋” 过程,每个节点(或者说每个线程)都在自省地观察,
            // 当条件满足,获取到了同步状态(即获得了锁),就可以从自旋过程退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)
            // 节点之间在循环检查的过程中基本不通信,而是简单地判断自己的前驱结点是否为头节点,这样就使得节点的释放符合FIFO
            for (;;) {
                //获取node 的前置节点
                final Node p = node.predecessor();
                /*
                 * 若node 的前置节点为头节点,且当前线程显式的获取锁成功(即成功获取同步状态),
                 * 则将节点node 设置为头节点,并退出循环(即退出自旋)
                 *
                 * 若p是头节点,有可能此时头节点释放了锁,那么节点p尝试调用tryAcquire 方法去竞争了一次锁
                 *
                 */
                if (p == head && tryAcquire(arg)) {//这一段表明:头节点是获取了锁的的节点
                    //若p获取锁成功,则更新头节点,并释放p的引用
                    //将node 设置为头节点
                    setHead(node);
                    //删除原先的头节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果p(node的前置节点)节点不是头节点或node竞争锁失败,那就先判断是否应该阻塞,若需要阻塞,则调用 parkAndCheckInterrupt 来阻塞当前线程
                //节点node获取锁(获取同步状态失败)失败,且node中的线程线程已经阻塞和被中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
                /*
                 * todo 从上面可以看出,只有前置节点是头节点的节点node(也可以说是线程)才能获得同步状态,这是为什么?
                 *      1)头节点是已经获得同步状态的节点,头节点的线程在释放了同步状态之后,会唤醒后序的节点(线程),
                 *         后继节点被唤醒后也要检查自己的前置节点是否是头节点
                 *      2)维持队列FIFO 的特性
                 *
                 *     当前线程是头节点的下一节点所关联的线程
                 *
                 * todo 注意:
                 *    若shouldParkAfterFailedAcquire(Node, Node)方法返回值为false,则acquireQueued(Node, int)方法为死循环,
                 *    所以acquireQueued(Node, int)方法会一直检查更新节点的状态值,直到当前节点的前驱节点状态值为SIGNAL,这是AQS约定的,
                 *    只有前继节点的waitStatus是SIGNAL,当前节点才可以安心的去阻塞。因为前继节点的waitStatus是SIGNAL,
                 *    就相当于当前节点告诉了它的前继节点,我将要去阻塞了,到时候请唤醒我。此时,shouldParkAfterFailedAcquire(Node, Node)方
                 *    法返回值为true
                 *
                 *
                 */
            }
        } finally {
            if (failed)
                //取消尝试获取锁的节点
                cancelAcquire(node);
        }
    }

1.5、shouldParkAfterFailedAcquire(Node pred, Node node) 方法

         该方法用来判断当前线程节点node是否应该阻塞,无非是想找到一个可靠(活着)的节点

         将其设置为SIGNAL,然后将当前线程节点node作为其的后置节点。

         shouldParkAfterFailedAcquire 方法结构如下:

         

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取前置节点pred 的waitStatus 的状态值
        int ws = pred.waitStatus;
        //若前置节点pred的状态是 SIGNAL,表明该前置节点是活动的,但其后继节点则此时可以安全的睡眠,
        // 表示后序节点node 的线程需要阻塞,即当前线程应该被阻塞,返回true
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             *
             * 这个节点已经设置状态要求释放(即其所关联的线程需要运行),所以它可以安全阻塞,也就是park。
             */
            return true;
        if (ws > 0) {//大于0的状态只有一个,即 CANCELLED
            //wsws = Node.CANCELLED,即node的前置节点是一个无效的节点,
            // 那么则跳过节点pred,查找未被阻塞和取消中断的节点(线程)
            /*
             * 从后往前查找
             * 如果节点 pred 处于取消状态,则跳过取消的节点,向前查找,直到node
             * 的前置节点pred 的 waitStatus 的值不大于0为止,并更新node的前置节点
             */
            do {

                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //找到距离最近,未被取消的节点作为当前节点node的前置节点,并更新该节点的后置节点为node
            pred.next = node;
        } else {
            /*
             * 若前置节点是正常节点,通过CAS将前置节点的状态修改为SIGNAL
             *
             * waitStatus的值为0或PROPAGATE,但当前节点需要一个SIGNAL 信号,此时还不能阻塞线程;
             * 将前置节点的状态值通过CAS 更新未 SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//设置 pred 的waitStatus 的值为-1
        }
        return false;
    }

1.6、parkAndCheckInterrupt() 

        该方法表示中断当前线程,

        在该方法中 this指代当前AQS对象,表示当前线程阻塞在那个对象上,后期

        可以通过jstack来看到,用于排查问题

        parkAndCheckInterrupt 方法结构如下:

             

1.7、cancelAcquire(Node node) 方法

        该方法作用是 取消AQS阻塞队列中的节点node,使node后续不再进锁资源的竞争

        cancelAcquire 方法结构如下:

          

private void cancelAcquire(Node node) {
        // 节点为null,直接结束
        if (node == null)
            return;

        node.thread = null;//设置节点node 关联的线程为null

        // Skip cancelled predecessors
        Node pred = node.prev;//获取当前节点的前置节点
        //若pred 的节点状态 waitStatus > 0,表示若pred节点也是取消状态,则跳过pred,让node的前置节点指向pred 的上一节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;

        node.waitStatus = Node.CANCELLED;//设置node为取消状态

        // 如果 node是尾节点,且删除node自己,并将node的前置节点设置尾tail操作成功,
        // 然后将node前置节点 pred 的后置节点设置为null
        if (node == tail && compareAndSetTail(node, pred)) {
            //CAS 将指定节点的后置节点设置为null
            compareAndSetNext(pred, predNext, null);
        } else {
            /**
             * 如果 node 不是尾节点或将node的后置节点设置尾节点操作失败,
             * 则后置节点pred 需要唤醒,尝试获取 pred 的下一链接
             */
            int ws;
            /**
             * 如果 pred 不是头节点,且(节点 pred的状态标记 waitStatus=-1,或将 waitStatus 的值设置为-1 操作成功)
             * 且 节点pred 关联的线程不为null,则将 pred 的后置节点设置为 node 的下一节点
             */
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    //将 pred 的后置节点设置为 node 的下一节点
                    compareAndSetNext(pred, predNext, next);
            } else {
                //唤醒node节点
                unparkSuccessor(node);
            }

            //node下一节点指向它自己,这时node是游离的状态,没有节点指向它,它也不指向其他节点,它关联的线程也是null,
            //此时的node可以被GC回收
            node.next = node; // help GC
        }
    }

 

         

2、互斥锁的释放过程

2.1、release() 方法

        release方法是AQS中互斥锁 释放锁的入口,在该方法中通过调用子类的tryRelease() 方法

        释放锁成功后,则调用unparkSuccessor() 方法唤醒AQS阻塞队列中下一阻塞的线程节点。

       注意:当前活动的节点(即持有锁的节点)一定是AQS阻塞队列中的头节点,这在

                 上边 acquireQueued 方法中可以发现

           release() 方法结构如下:

                

        

2.2、unparkSuccessor(Node node) 方法

         该方法的作用是唤醒当前节点node的后置节点

          注意:当前node节点是活跃节点(即持有锁的节点)且node一定是头节点

          unparkSuccessor() 方法结构如下:

                

private void unparkSuccessor(Node node) {
        /*
         * 如果状态是消极的(即,可能需要信号),尝试在预期信号中清除。如果这个失败或者状态被等待线程改变,它是OK的。
         */
        int ws = node.waitStatus;//获取节点node 的状态(标记)
        /**
         * 开始唤醒后继节点,当前节点是头节点,且当前节点是活跃的,即节点状态为SIGNAL=-1,然后
         * CAS将其设置为0,表示当前节点已经响应了这一次的唤醒操作
         */
        if (ws < 0)
            //将 node.waitStatus 的值设置为0
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * unpark的线程保存在后续节点中,通常是下一个节点(也可能不是下一个节点)。
         * 但如果下一个节点waitStatus的值表示取消或明显为空,从尾部向后遍历,以找到实际的
         * 未取消的后续节点。
         *
         * unpark 表示未被占用/使用的线程
         */
        /**
         * todo 这里是核心:
         *    取当前头节点的后继节点作为唤醒节点,但是请注意if中的判断条件
         */
        Node s = node.next;//node 节点的后序节点
        if (s == null ||          //这里为什么可能会为null? 因为在enq()入队列方法中先更新的是尾节点tail,然后才更新旧的tail.next,所以node.next可能一瞬间为null
                s.waitStatus > 0) {//s节点是无效节点?因为前面的enq()入队列方法更新无效节点那一段不是原子性的,所以多线程下node.next可能指向的还是无效的节点
            s = null;
            /**
             * todo
             *  tail节点一定是最新入队列的节点,从队列的尾部向前遍历,
             *  找到node节点后面第一个有效的节点(node节点的后继的第一个有效节点)
             */
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    //找到需要唤醒的节点
                    s = t;
        }
        //此时s就是要唤醒的节点
        if (s != null)
            //唤醒节点s 中的线程
            LockSupport.unpark(s.thread);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值