AQS共享锁原理分析

1,859 阅读5分钟

一、引言

       共享锁区别于独占锁,多个线程可以同时持有锁,java中的Semaphore、CountdownLatch、ReentrantReadWriteLock中的readerLock都是共享锁,独占锁的原理在juejin.cn/post/684490…中已经介绍过,建议读者先阅读这一部分。

二、共享锁的获取

     共享锁的获取分响应中断和忽略中断两种,下面介绍响应中断共享锁获取。

2.1. 响应中断获取共享锁

     以CountdownLatch为例,await()底层调用的是AQS.acquireSharedInterruptibly(),在响应中断的条件下获取共享锁。

//在获取到指定锁之前,线程阻塞。
public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

AQS.acquireSharedInterruptibly(int arg):获取共享锁,当线程被中断时放弃。

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果线程被中断,抛出InterruptedException,用于业务线程catch该异常,可用于是否停止线程
        //的运行
        if (Thread.interrupted())
            throw new InterruptedException();
        //获取共享锁,由子类实现
        if (tryAcquireShared(arg) < 0)
            //获取共享锁,当线程被中断时抛出InterruptedException,放弃锁的获取。
            doAcquireSharedInterruptibly(arg);
    }

tryAcquireShared()由子类实现,返回值:

  • 大于0:当前线程获取锁成功,后续线程获取锁可能成功(由于竞争);
  • 0:当前线程获取锁成功,后续线程获取锁失败;
  • 小于0:当前线程获取锁失败。
AQS.doAcquireSharedInterruptibly(int arg):

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //以获取共享锁的方式,将当前线程插入等待队列,该方法在独占锁的分析中已经讲解。
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                //获取当前节点的pre节点
                final Node p = node.predecessor();
                //只有pre是head,才能轮到当前节点获取锁。
                if (p == head) {
                    //获取锁,由子类实现
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //获取锁成功,重新设置head节点,并传播状态。
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //竞争锁失败后,是否能挂起当前线程;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //挂起该线程,当线程被唤醒后,返回中断标志位。
                    //如果线程被中断,抛出异常,终止该线程对锁的竞争。
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                //放弃竞争锁。
                cancelAcquire(node);
        }
    }

AQS.setHeadAndPropagate(Node node, int propagate): 当前线程获取共享锁成功,需要重新设置head节点,并传播状态。

         这里区别与独占锁,独占锁中获取锁成功后,只是设置head节点,只有当锁释放时才会唤醒后继节点;共享锁由于可以多个线程同时持有锁,因此在一个线程获取锁成功后就应该唤醒后继节点。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // 记录当前的head节点
        setHead(node); //由于当前线程已经成功获取锁,所以可以将node->head.
        /*
         *如果当前线程获取到锁(propagate>0),|| head=null || head.waitStatus<0,需要唤醒后继节点。
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //s为null说明当前已经没有其他锁尝试获取锁,s.isShared()说明当前是共享锁状态
            //对于上面的任意一种情况满足时,执行doReleaseShared(),用于唤醒后继线程。
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

AQS.doReleaseShared:释放锁,并唤醒后继节点。

private void doReleaseShared() {
        /*
         * 共享锁下,可能同时有多个线程持有一把锁,意味着同时会有多个线程获取锁/释放锁。
         * 确保锁的释放,即使此时可能有其他线程获取/释放锁。
         */
        for (;;) {
            Node h = head;
            //h != null && h != tail 说明此时至少还有一个线程在等待锁。
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果h.waitStatus=SIGNAL,唤醒其后继节点。
                if (ws == Node.SIGNAL) {
                    //由于此时其他线程也在释放锁,都在执行doReleaseShared(),一个线程执行compareAndSetWaitStatus成功即可,并唤醒后继节点。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                } //如果后继节点不需要唤醒,设置状态。
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // 如果在此期间,head节点变动了,继续循环,唤醒后继节点。
                break;
        }
    }

2.2 忽略中断获取共享锁

        以下是忽略中断获取共享锁的入口,与响应中断获取共享锁不同,前者对于中断的处理只是将中段位置位,后者则是通过抛出InterruptedException的形式推出锁的竞争。

AQS.doAcquireShared(int arg) :

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        //如果线程被中断,中断自己。
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //线程竞争锁失败后,是否能够挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt()) //挂起线程并检查中断位
                    //如果线程被中断,设置interrupted
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

三、共享锁的释放

AQS.releaseShared(int arg):

public final boolean releaseShared(int arg) {
        //释放线程,tryReleaseShared返回true表示后继线程可能可以成功获取锁,因此需要调用doReleaseShared(),唤醒后继线程;
        //否则,没有必要唤醒后继线程,直接返回即可。
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

        独占锁在释放锁时,当重入次数=0时,代表当前线程已经完全释放锁,此时会调用unparkSuccessor()方法,唤醒后继线程。而共享锁模式下,只有当后继线程可能获取锁成功时(由tryReleaseShared定义,在子类中实现),才唤醒后继线程。

至此AQS共享锁的原理分析完毕。


参考:segmentfault.com/a/119000001…