AQS(二):共享锁的获取和释放

一、什么是共享锁?

       同一时间点允许多个线程同时持有的锁,(请参考前边锁的分类)

二、AQS原理之共享锁的获取和释放流程

1、共享锁的获取过程

1.1、acquireShared(int arg) 方法

         该方法用于获取共享锁(读锁),在方法中先调用 tryAcquireShared() 方法判断是否获取共

        享锁成功,若tryAcquireShared() 获取锁失败,则调用 doAcquireShared() 方法将当前线程

        放入AQS阻塞队列中去阻塞。

        tryAcquireShared() 由子类实现,他的实现类有:Semaphore.FairSync、

         Semaphore.NonfairSync、CountDownLatch.Sync、ReentrantReadWriteLock.Sync,

          tryAcquireShared() 方法的具体事项在后边分析各个工具类时再说。

         acquireShared方法也是一个模版方法。

         acquireShared() 方法结构如下:

                  

1.2、 doAcquireShared(int arg) 方法

          该方法是获取共享锁的具体实现;该方法主要功能是先调用addWaiter(Node.SHARED)

          将当前线程封装成线程节点,并把线程节点添加到AQS阻塞队列中;然后通过自旋的

          方式不停的尝试获取读锁(在自旋过程中会从后往前遍历AQS队列,并更新当前节点的

          前置节点),若自旋过程中出现异常,则会调用 cancelAcquire(node) 方法取消当前节点。

            doAcquireShared(int arg)  方法代码如下:

private void doAcquireShared(int arg) {
        //根据当前线程创建共享模式的节点,并添加等阻塞队列中
        //跟互斥锁方法一样,只是这里传入的参数是“共享模式”
        final Node node = addWaiter(Node.SHARED);//优化前置
        //标志释放成功获取同步状态
        boolean failed = true;
        try {
            //中断状态
            boolean interrupted = false;
            /**
             * 自旋,直到 tryAcquireShared() 方法成功获取到同步状态(锁)为止
             */
            for (;;) {
                //获取node 的前置节点
                final Node p = node.predecessor();
                //判断前置节点p 是否是头节点,若是头节点则调用 tryAcquireShared() 获取读锁
                //刚好当前线程节点在头节点后边,此时头结点可能会很快释放锁,那么当前线程节点需要尝试获取锁
                if (p == head) {
                    //获取锁,由子类实现
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //若当前线程的获取锁成功,则调用 setHeadAndPropagate() 更新头节点并尝试唤醒后边的
                        // 共享节点(因为共享锁可以同时被多个线程持有),
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC 删除当前节点的前置节点
                        /**
                         * 如果当前线程在等待过程中发生了中断,因为中断而被唤醒,那么置位当前线程的中断标志位
                         */
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //当前线程获取同步状态(锁)失败,则阻塞当前线程,继续循环检测
                //更新当前节点的前置节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

1.3、setHeadAndPropagate(Node node, int propagate) 方法

        该方法作用是:更新AQS阻塞队列的头节点(将当前获取锁的线程节点设置为头节点),

        并尝试获取后续的共享节点。

        如果当前的同步状态(或资源数)大于0,则判断后继节点是否是共享模式,如果是共

       享模式,那么就会调用doReleaseShared()方法,则直接对后继节点进行唤醒操作,也

       就是说每次获取同步状态的线程都有可能通过setHeadAndPropagate(Node, int)方法来唤

       醒其他线程,进而修改同步状态,从而激发多个线程并发的运行。

          setHeadAndPropagate 方法结构如下:

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //将当前获取锁的节点node 设置为队列头
        setHead(node);
        /*
         * 尝试向下一个队列节点发送信号,如果:
         * 传播由调用者指示,或被前一个操作记录(或在setHead之后)
         * (注意:这使用了waitStatus的符号检查,因为PROPAGATE状态可能会转换为signal。)
         * 和
         * 下一个节点在共享模式下等待,或者我们不知道,因为它看起来是空的
         *
         * 这两种检查的保守性可能会导致“不必要的唤醒”,但只有当有多个“竞争获取/释放”时,所以大多数人现在或很快就需要信号。
         */
        if (propagate > 0 ||   //信号量还有多余,则直接唤醒后继节点
                h == null ||   //不可能发送
                h.waitStatus < 0 ||  //SIGNALL(表示必须唤醒后继节点) 直接唤醒
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

         

1.4、doReleaseShared() 方法

        该方法主要功能是释放共享锁节点。

        doReleaseShared 方法结构如下:

          

private void doReleaseShared() {
        /*
         * 确保一个发布得到传播,即使有其他正在获取/发布。这在通常的方式试图unpark继任者的头节点,
         * 如果它需要信号(SIGNAL)。但如果它没有,状态设置为PROPAGATE ,以确保在发布时,传播继续。
         * 此外,我们必须循环,以防止在执行此操作时添加了一个新节点。另外,与 unpark继任的其
         * 他用途不同,我们需要知道CAS重置状态是否失败,如果失败,则重新检查。
         */
        for (;;) {
            //获取头节点head
            //在自旋遍历过程中头结点head可能被其他线程修改,所以这里需要一个临时变量h
            Node h = head;
            if (h != null && h != tail) {//AQS队列不为null
                //获取头节点的的waitStatus值(节点等待状态,标记值)
                int ws = h.waitStatus;
                /**
                 * 若 ws 的值等于 Node.SIGNAL,表示需要唤醒后续节点,因为 SIGNAL 语义是必须唤醒后续节点
                 */
                if (ws == Node.SIGNAL) {
                    //CAS原子性修改节点 h 状态值为由SIGNAL修改为0,即将节点h设置为取消状态,
                    //若修改成功,表示唤醒成功
                    //CAS执行的场景:并发下多个线程同时修改节点h
                    //在共享模式下,节点h的state值表明了可用的资源数,如:若state=2,表示可用的资源数为2,需要唤醒后边2个共享节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases  CAS执行失败,循环来重新检查
                    /**
                     * 上面CAS操作成功 ,则唤醒 h 的后继节点,
                     */
                    unparkSuccessor(h);
                }
                //Node.PROPAGATE: 用于共享锁唤醒后继共享节点的标志位,该标志位可以避免h的状态被修改为0后
                // 导致后边的共享节点无法被唤醒的情况
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS  CAS失败,进行下一次循环
            }
            //执行唤醒操作后,若h还等于头节点 head,即头结点没有被其他线程改变,则说明队列已经遍历完了,则退出循环
            if (h == head)                   // loop if head changed  换头循环
                break;
        }
    }

2、共享锁的释放过程

2.1、releaseShared(int arg) 方法

        该方法作用是释放共享锁,通过调用子类的tryReleaseShared() 来判断子类具体的锁释放

        是否成功,若子类释放锁成功,则调用方法 doReleaseShared() 去释放AQS中的当前共享

        锁节点,并唤醒后继的共享节点。

        方法 tryReleaseShared() 的子类实现有 Semaphore.Sync、CountDownLatch.Sync、

        ReentrantReadWriteLock.Sync

         eleaseShared 方法结构如下:

                

2.2、doReleaseShared() 方法同上边 1.4

三、AQS常见的问题

1、AQS中头节点head 为什么是伪节点?     AQS可以没有head节点,设计之初设计head伪

     节点只是为了方便操作

    如ReetrantLock 中锁资源时,是否考虑唤醒后继节点,有了head表示当前持有锁的线程

    节点,操作起来 就会方便很多,如果head节点节点的状态不是-1,则就不需要去唤醒后

    继节点;唤醒后继节点时,需要找到head.next 节点,如果head.next=null或取消了,此时

    需要遍历整个双向链表(从后往前遍历),找到一个离head最近的未被取消的节点node,

   这样可以规避一些不必要的唤醒操作(有head节点的链表,state=-1de 节点管理的是其后边

    的节点)。

   若不使用虚拟节点head,节点挂起时,就设置挂起节点的状态为-1,当该挂起

   的节点被唤醒时,就把自己的状态由-1改为0(自己管理自己),这种方式可行。

    

2、AQS为什么使用双向链表?

      双向链表是为了更容易操作node节点,既可以从前往后遍历,也可以从后往前

      遍历,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值