好不容易说明白AQS,面试官却还要我说应用场景,我只好又讲了CountDownLatch ~~~

Posted CRUD速写大师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了好不容易说明白AQS,面试官却还要我说应用场景,我只好又讲了CountDownLatch ~~~相关的知识,希望对你有一定的参考价值。



前言

博主 常年游荡于牛客面经区,总结了字节、阿里、百度、腾讯、美团等等大厂的高频考题,之后会逐步分享给大家,期待各位的关注、点赞!

在这里插入图片描述



CountdownLatch源码讲解

CountdownLatch主要原理依赖于AQS,还不了解AQS的小伙伴可以看我的这篇博文了解了解,很容易去上手AQS这个面试大考点

AQS快速上手

话不多说,开撸,哦不,开讲!!!


CountDownLatch主要有两个方法:①:await(),②:countDown();

那我就不先买个关子了,我直接说:
调用await()方法的线程会被阻塞,直到计数器 减到 0 的时候,才能继续往下执行;
countDown():将计数器减一,
相信小伙伴们看完了我的源码讲解之后就会明白了。

但是我们先从CountDownLatch构造函数看起



CountDownLatch构造函数

CountDownLatch countDownLatch = new CountDownLatch(2);

这里可以看到CountDownLatch 的构造函数其实是new的一个Sync,且将我们传入的int类型值也作为了Sync的参数

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

我们再点进来看,可见Sync继承了 AbstractQueuedSynchronizer 即AQS

而Sync的有参构造调用了setState(count);方法

在这里插入图片描述

再看看setState方法,其实说白了就是将我们CountDownLatch传入的int值作为了AQS的同步状态。

在这里插入图片描述

到这再强烈建议大家先入手AQS,入手了之后再看这并发包下的工具类真的不要太简单!!!


AQS快速上手



countDown()图示、源码讲解

再来看看 countDown 方法

public void countDown() {
    sync.releaseShared(1);
}

这里调用了sync的releaseShared方法,传入了arg = 1

而我们可以看到releaseShared涉及到了两个方法 tryReleaseShared(1)和doReleaseShared()

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

先看tryReleaseShared(1)

在这里插入图片描述

这源码相信给位小伙伴一看都能懂把,那我就简要总结下!

  • 调用getState()方法获取同步状态,如果同步状态为0,则返回false,结合下面的代码看,意味着不能在减同步状态了
  • 如果不为0,也可以说是大于0,那么用一个int变量记录将同步状态减一后的值
  • 最后同步CAS设置同步状态为减一后的值,如果设置失败就自旋重试,如果成功就看减一后的值是不是0

不了解CAS的也可以看看我的这篇博客,超高点击量:面试被问到CAS原理,触及知识盲区,脸都绿了!

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

结合之前源码看,如果减一后的同步状态为0,那么就会调用 doReleaseShared()方法

在这里插入图片描述

private void doReleaseShared() {
    for (;;) {
      //获取头结点
        Node h = head;
        if (h != null && h != tail) {
          //获取head结点的等待状态
            int ws = h.waitStatus;
          //如果head结点的状态为SIGNAL, 表明后面有人在排队
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                  //直到CAS成功
                    continue;            
                //再去唤醒后继结点;在独占锁的时候有说明,这里就不多说了; 
              unparkSuccessor(h);
            }
          //如果head结点的状态为0, 表明此时后面没人在排队, 就只是将head状态修改为PROPAGATE
     	else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                 //直到CAS成功
                 continue;                
        }
        if (h == head)                   
            break;
    }
}

可能小伙伴们看了源码也没太懂,那我小小总结一下

  • 当同步状态为0的时候才会去调用,doReleaseShared()方法
    • 如果同步状态为0,说明锁没有线程占用
  • 那么就涉及到doReleaseShared()方法,去看该线程后面有没有线程排队
    • 如果有线程排队,那么upark将其唤醒,并且有没有节点都要更新当前节点的状态


await()图示、源码讲解

await()方法调用的 sync的acquireSharedInterruptibly(1)方法

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

点进来看

第一个if没啥好说的,如果线程被打断了就要抛异常

主要是第二个if,涉及到tryAcquireShared(1),doAcquireSharedInterruptibly(1)方法

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared(1)跟传入的参数无关,就是看当前的同步状态是否为0,如果为0返回1,不为0返回-1

那么有小伙伴问了,为什么要这么返回呢???

不要着急,我们接着看第二个方法

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

doAcquireSharedInterruptibly

别的都可以不看,关键就在于这个for(;;)循环
我们可以看到,如果想要退出for循环,必须满足 (p == head) && (r >= 0),也就是当前节点等于头节点,且同步状态为0

tryAcquireShared我们上面介绍过
温馨提示:建议了解AQS:AQS快速上手

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

ok,相信能看到这里的小伙伴可能差那么一点就能明白,那我再给各位小伙伴总结一下。
创作不易,希望小伙伴们可以一键三连多多支持!!! 😁



总结

就像我开头说的,
调用await()方法的线程会被阻塞,直到计数器 减到 0 的时候,才能继续往下执行;
countDown():将计数器减一。

结合上面的源码讲解,先说await()

  • 如果计数器不为0,那么tryAcquireShared返回的就一定为1,那么 r >= 0就不会满足,那么就无法退出,会一直进行for循环即起到阻塞作用

再说countDown()

  • 每调用一次countDown()方法就会去利用CAS将计数器减一

  • 当同步状态为0的时候才会去调用,doReleaseShared()方法

    • 如果同步状态为0,说明锁没有线程占用
  • 那么就涉及到doReleaseShared()方法,去看该线程后面有没有线程排队

    • 如果有线程排队,那么upark将其唤醒,并且有没有节点都要更新当前节点的状态


CSDN独家福利降临!!!


最近CSDN有个独家出品的活动,也就是下面的《Java的全栈知识图谱》,路线规划的非常详细,尺寸 是870mm x 560mm 小伙伴们可以按照上面的流程进行系统的学习,不要自己随便找本书乱学,要系统的有规律的学习,这样基础才是最扎实的,在我们这行,《基础不牢,地动山摇》尤其明显。

最后,如果有兴趣的小伙伴们可以酌情购买,为自己的未来铺好道路!!!


在这里插入图片描述


最后

我是 CRUD速写大师,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~~~

分享大纲

大厂面试题专栏


Java从入门到入坟学习路线目录索引


开源爬虫实例教程目录索引

更多精彩内容分享,请点击 Hello World (●’◡’●)


在这里插入图片描述

以上是关于好不容易说明白AQS,面试官却还要我说应用场景,我只好又讲了CountDownLatch ~~~的主要内容,如果未能解决你的问题,请参考以下文章

程序员从BAT跳槽,却不会写二分查找,结果面试官却被喷?

面试官:小伙子,你给我说一下Java Exception 和 Error 的区别吧?

如果我说熟悉SpringBoot 面试官会怎么问?

离职后,我只想找份功能测试,面试官却把我逼上绝境……

22:00,女友问我还要忙多久?我说一会儿!结果...

面试官:小伙子,你给我说一下HashMap 为什么线程不安全?