AQS分析第四篇(借助CountDownLatch探索AQS共享模式的实现原理)

上一篇我们通过ReentrantLock分析了如何AQS独占模式的实现原理。这一篇我们将根据CountDownLatch探索共享模式的实现原理,如果你认真看完上一篇,那该篇的内容将更容易理解。主要分析以下几个问题?

一、思考

  • 问题一:CountDownLatch提供了怎样的功能?其实现原理是什么?AQS独占模式是怎样实现的?
  • 问题二:共享锁和独占锁的区别是什么?

二、源码分析

因为CountDownLatch是借助AQS实现的,所以其必然有一个内部类继承AQS并实现tryAcquireShared(获取共享锁)和tryReleaseShared(释放共享锁)方法,这是使用AQS的唯一方式。我们去论证这一点:

在介绍CountDownLatch实现之前,我们先了解一下CountDownLatch的一般使用方式:

CountDownLatch latch=new CountDownLatch(n);


//A线程中等待
Thread  a=new Thread(()->{
    latch.await();
    doSomeThing();
});

....可能有多个线程同时调用latch.await();

//其他线程countDown
Thread  b=new Thread(()->{
    doSomeThing();
    latch.countDown();
});



Thread  c=new Thread(()->{
    doSomeThing();
    latch.await();
});

......

Thread  n=new Thread(()->{
    doSomeThing();
    latch.await();
});

接下来我们将从CountDownLathc的定义,await方法,countDown方法取剖析其实现原理,以及AQS共享模式的实现原理。

2.1CountDownLatch的定义

Method:CountDownLatch(int)

//CountDownLatch初始化
public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        //详情请看下文Sync构造函数
        this.sync = new Sync(count);
}

Method:Sync(int)

//在CountDownLatch初始化时调用AQS中setState方法将AQS中state值初始化为count
Sync(int count) {
            setState(count);
}

2.2CountDownLatch等待(这里只分析不限时等待,后面会专门出一篇关于AQS中限时等待的博文)

Method:await()

//使该线程等待直到以下两种情况之一发生:
//1.其他线程调用countDown方法使count变为0
//2.其他线程调用Thread.interrupt方法中断该线程
public void await() throws InterruptedException {
    //具体请看下文acquireSharedInterruptibly(int)方法 
    sync.acquireSharedInterruptibly(1);
}

Method:AbstractQueuedSynchronizer.acquireSharedInterruptibly(int)

//通过共享模式获取,如果线程被中断则停止。

//如果当前线程被中断,将抛出InterruptedException。
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //因为支持可中断,所以先检查线程是否中断

        if (Thread.interrupted())
            throw new InterruptedException();
        //先调用子类的tryAcquireShared尝试以共享模式获取,如果获取失败(该方法返回值小于0),则进
        //阻塞获取

        //详情请看下文tryAcquireShared和doAcquireSharedInterruptibly方法
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

Method:Sync.tryAcquireShared(int)

//尝试获取,如果stage为0,则获取成功,否则获取失败

//注意state被初始化为n,这里的语义就是当n没有减为0时,认为该线程是不可获取成功的
protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
}

Method:doAcquireSharedInterruptibly(arg)

//以共享可中断模式进行获取
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //addWaiter方法请看【AQS分析第三篇】

        //创建新的代表当前线程的Node入队

        //这里与独占模式的区别在于Node.nextWaiter=Node.SHARED
        final Node node = addWaiter(Node.SHARED);
        //最终是否获取失败
        boolean failed = true;
        try {
            //自旋,当被唤醒之后,再次检查之前被阻塞的原因现在是否满足
            for (;;) {
                final Node p = node.predecessor();
                //head为当前获取成功的线程,会释放其后继线程(这里后继线程是自己,所以尝试获取)
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //这里r>==0,说明state为0(已有n个线程调用了countDown方法)
                    if (r >= 0) {
                      
                        //如果获取成功,将当前Node设置为队头节点

                        //这里与独占模式不同的是,独占模式只是调用setHead(请看上一篇)设 
                        //置了队头,请看下文setHeadAndPropagate分析其除了设置队头还需要干什么
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //下面这段阻塞前处理和线程阻塞的代码同【AQS分析第三篇】,这里不再赘述

                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            //这里同上一篇ReentrantLock分析一样,如果tryAcquireShared方法异常,则取消等待
            if (failed)
                cancelAcquire(node);
        }
}

Method:AbstactQueuedSynchronizer.setHeadAndPropagate(Node,int)

//在分析该方法之前我们先总结一下:
//到目前为止,当前线程尝试获取成功(tryAcquireShared方法返回值大于等于0,即state值为0),那么此时 
//我们肯定要做的是设置当前线程为队头线程(这一点同独占锁,因为无论什么模式head代表获取成功的线程)
//那除了设置队头还需要做什么。

//因为可能有多个线程
//会调用latch.await方法进行等待,等待的条件是state不为0。当能走到这个方法,说明tryAcquiredShared方法返回值大于等于0,说明此时state为0,所
//以我们需要将所有在await方法上阻塞的线程全部唤醒。所以当前被唤醒的线程是有义务去唤醒其后继的所有
//线程的(传播机制)。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       
        //下面列举了很多需要传播的情况,这里我们只看propagate>0的情况,该参数表明了是否需要传播
        //大于0表示需要,在CountDownLatch中,当state为0时,tryAcquireShared返回的是1,即需要传播。
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //如果当前节点的后继节点是共享模式,唤醒当前节点的后继节点(传播),在这里我们第一次
            //使用了nextWaiter属性,请看下文isShared方法(判断需不需要传播唤醒后继节点)
            Node s = node.next;
            if (s == null || s.isShared())
                //需要唤醒则调用doReleaseShared依次唤醒后继节点
                //请看下文doReleaseShared方法
                doReleaseShared();
        }
}

Method:isShared()

//返回true则代表该节点是需要被传播唤醒的
final boolean isShared() {
            return nextWaiter == SHARED;
}

Method:doReleaseShared()

//该方法在CountDownLatch的实现中会被调用多次:
//1.当某个线程调用latch.countDown之后,state变为0,调用该方法唤醒head后继线程
//2.当某个阻塞线程从park返回之后,发现tryAcquireShared方法返回true,会调用setHeadAndPropagate
//,这个方法中会调用该方法唤醒head后继线程

//该方法将确保释放操作在队列中传播,即使有其他acquire/release操作在执行中。

private void doReleaseShared() {
        //无限传播
        for (;;) {
            Node h = head;
            //该if分支不满足情况:
            //h==null或者h==tail:这代表队列中无Node或者只有一个Node节点(当前线程),则无须继 
            //续传播。

            //当队列中阻塞线程个数>1时,则需要尝试唤醒head后继节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表明需要唤醒后继节点
                if (ws == Node.SIGNAL) {
                    //CAS判断是否有其他操作已经唤醒head后继节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        //如果确实有其他操作已经唤醒head后继节点,则再次循环传播释放
                        continue;            // loop to recheck cases
                    //唤醒head后继节点 
                    unparkSuccessor(h);
                }
                //ws==0表明无阻塞线程被添加到head之后
                //CAS设置ws==PROPAGATE:让下一个调用acquireShared的线程可以被无条件传播,因为
                //这里释放了锁,但是并无唤醒任何线程,所以下一个线程当然可以无条件传播

                //CAS失败,可能已经有其它线程加入head之后,循环传播唤醒
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            

            //当h==head,说明被唤醒的线程再次尝试获取依然失败。或者无后继节点需要唤醒,则退出循 
            //环。
            if (h == head)                   // loop if head changed
                break;
        }
    }

2.3CountDownLatch.countDown

Method:CountDownLatch.countDown()

//将count值减1,当count=0时唤醒所有在latch.latch上等待的线程
public void countDown() {
        //具体请看下文AbstractQueuedSynchronizer.releaseShared(int)方法
        sync.releaseShared(1);
}

Method:AbstractQueuedSynchronizer.releaseShared(int)

public final boolean releaseShared(int arg) {
        //尝试以共享模式进行释放,当返回true时(count为0),唤醒素有才latch.await上等待的线程,
        //请看下文tryReleaseShared方法。
        if (tryReleaseShared(arg)) {
           //以传播方式唤醒所有阻塞线程,请看上文对该方法的解释
            doReleaseShared();
            return true;
        }
        return false;
}

Method:Sync.tryReleaseShared()

//该方法会线程安全的对count(state)进行减一操作,当count为0之后
//返回值:true代表需要唤醒队列中再latch.await上等待的线程
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                //当state已经为0,说明已经执行过唤醒操作返回false
                if (c == 0)
                    return false;
                int nextc = c-1;
                //CAS加失败重试
                if (compareAndSetState(c, nextc))
                    //如果当前线程将count-1之后,count为0,则唤醒所有等待线程
                    return nextc == 0;
            }
}

至此我们已经看完CountDownLatch的源码实现。接下来我们先总结一下:

CountDownLatch初始化时,会将state设置为n,所有在CountDownLatch上调用await方法的线程都将等待直到有n个线程调用countDown方法将state值减为0。当state值为0时,AQS会通过传播机制依次唤醒这些在队列中阻塞的线程。但是这个唤醒操作,不会等待当前线程执行完成再去操作,只要当前线程获取成功,就先唤醒后继节点,然后继续执行自己的方法。

三、开篇解答

  • 问题一:CountDownLatch提供了怎样的功能?其实现原理是什么?

答:CountDownLatch可以让一个或一组线程等待其他一些线程进行完某些操作后执行。原理上借助AQS共享模式进行实现,详情请看上文源码分析。

  • 问题二:共享模式和独占模式的区别是什么?

答:我们先分析一下CountDownLatch中共享模式体现在哪里,当调用CountDownLatch.countDown()方法时,只是CAS加失败重试的机制,这里不存在对资源的获取操作,所以不会体现共享模式。当调用CountDownLatch.await方法时,如果state不为0,那么线程将阻塞,其实这里和独占的ReentrantLock认为state不为0类似,也并体现不出共享性。但是当state为0时,CountDownLatch中所有在await方法上阻塞的线程都将获取成功,而ReentrantLock中只有一个线程可以在lock上获取成功,这里变体现了独占和共享模式的区别。总结一下如下:

独占模式:当state为0时,下一个获取成功的线程将独占state状态,其他在等待这个条件(state=0)的线程无法获取成功。

共享模式:当state为0时,所有在等待这个条件(state=0)的线程都将获取成功。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值