AQS中共享锁源码解析

AQS中共享锁源码解析

AQS中的非共享锁,也就是我们所说的排他锁,相信大家都不陌生,比如ReentrantLock中的公平锁以及非公平锁。最近小编心血来潮看了看AQS中的共享锁,于是写点东西分享下,欢迎指正。 AQS中共享锁的实现,其实很多,比如大家耳熟能详的CountDownLatch,ReentrantReadWriteLock,读写锁大部分也都是用了共享锁的思想。还是按照以前看互斥锁的思路来看。

    public final void acquireShared(int arg) {
        //尝试获取锁,如果没有获取到,那么就进行下边的逻辑,这里尝试获取锁的方式有很多,
        //比如CountDownLatch中就是判断AQS中的state是否为0,也就是初始化时需要CountDown
        //的次数
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

下边来看具体逻辑:

    private void doAcquireShared(int arg) {
        //将该线程加入CLH队列中,这里Node的类型小编趟过坑,共享类型和排他类型,都会放入这个
        //类型,但是这个类型只是在Node中的nextWaiter中,不会在CLH队列中,只是一个标识,标
        //识是共享模式还是排他模式
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
        	//这个字段是判断在自旋过程中是否被中断,也就是@1的位置
            boolean interrupted = false;
            for (;;) {
            	//找到当前Node的前驱节点
                final Node p = node.predecessor();
                //如果前驱就是head,那么就尝试获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //获取成功,就开始改变头节点,唤醒后继节点了
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //如果不是头节点,那么就是喜闻乐见的流程了,这里shouldParkAfterFailedAcquire
                //大家应该不会陌生,跟排他锁中流程类似,如果当前节点的前一个节点waitStatus为
                //SIGNAL,那么我可以放心的将线程挂起了。但是如果不是,那么会一直寻找到有效的
                //前驱节点(在前驱节点是CANCELLED的情况下),或者尝试将前驱节点变成SIGNAL,
                //最后挂起线程,等待被唤醒。被唤醒后,再次在for循环中去check前驱是不是头
                //节点,再尝试获取锁。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;                     //@1
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

看完上述源码,相信大家已经有了一个大概流程上的认识了,下边继续看setHeadAndPropagate()方法又做了什么:

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //改变头节点
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //找到头节点的下一个节点,如果还是共享节点,那么好,开始你的表演
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    private void doReleaseShared() {
        //进入循环
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                //如果头节点的状态为SIGNAL,那么就唤醒头节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases

                    //唤醒后继节点,就是传说中的 LockSupport.unpark()
                    unparkSuccessor(h);
                }
                //这种情况,相信很多人不理解,我当时也是一脸懵逼,但是这里我说下我的拙见吧
                //如果不对希望大家能够指正我,我把邮箱留在下方大家可以探讨,因为disqus有
                //的人可能被墙了。
                //Node.PROPAGATE这个状态,很疑惑是干嘛的。我的理解是,在多线程情况下,
                //head的waitStatus是有可能为0的,比如CountDownLatch多处await()的
                //时候,直接获取到锁了,那么这种情况下后继节点是不需要唤醒的
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果头节点发生了改变,那么继续循环
            if (h == head)                   // loop if head changed
                break;
        }
    }

说到这里,尝试获取锁的过程就说完了,其实共享锁和排他锁的区别可以清楚的看到,就是锁的状态是可以共享的,获取到锁的节点会依次唤醒后继节点(这里显而易见,是按照CLH队列中添加的顺序依次向后唤醒),因此,依次会有多个线程获取到锁并进行后续操作。这就是共享的实质。

说完获取锁,那么再看释放锁:

    public final boolean releaseShared(int arg) {
        //尝试释放锁
        if (tryReleaseShared(arg)) {
            //释放成功,那么就唤醒后继节点
            doReleaseShared();
            return true;
        }
        return false;
    }

看到这里大家可能会有疑问,为什么release的时候又调用了之前见过的doReleaseShared()方法了呢。拿CountDownLatch为例,countDown()方法会将AQS中的state减去1,这时候所有调用await()方法的线程都会阻塞,直到state减到0。那么减到0后怎么办呢,那么就在这里,调用doReleaseShared()唤醒被阻塞的线程(也就是调用await()的线程,继续执行,讲到这里,CountDownLatch内部实现的机制相信大家也就知道了。

本文为作者原创,转载请注明出处 。邮箱:568718043@qq.com