当前位置: 移动技术网 > IT编程>开发语言>Java > CountDownLatch是个啥?

CountDownLatch是个啥?

2020年04月11日  | 移动技术网IT编程  | 我要评论

文章篇幅较短,对于一些aqs的顶级方法例如releaseshared并没有做过深的讲解,因为这些算是aqs的范畴,关于aqs可以看下另一篇文章——aqs

countdownlatch一般被称作"计数器",作用大致就是数量达到了某个点之后计数结束,才能继续往下走。可以用作流程控制之类的作用,大流程分成多个子流程,然后大流程在子流程全部结束之前不动(子流程最好是相互独立的,除非能很好的控制两个流程的关联关系),子流程全部结束后大流程开始操作。

 很抽象,小问题,下方的两节或许能让你理解countdownlatch的用法和内部的实现。

1.countdownlatch的使用

  假设现在,我们要起一个3块钱的集资项目,并且限定每个人一次只能捐1块钱当募集到3块钱的时候立马就把这笔钱捐给我自己,如果凑齐之后你还想捐,那么我会跟你说,项目已经完成了,你这一块钱我不受理,自己去买雪糕吃吧;如果没凑齐,那么我这个募集箱就一直挂在这里。这个场景用countdownlatch可以很契合的模拟出来。

  字数也不凑了,直接看demo例子吧

public static void main(string[] args) throws interruptedexception {
    
    // 集资项目==========>启动,目标3块钱
    countdownlatch countdownlatch = new countdownlatch(3);
    threadpoolexecutor executor = threadpoolprovider.getinstance();
    executor.execute(() -> {
        try {
            timeunit.milliseconds.sleep(100);
            system.err.println("张1准备捐一块钱");
            countdownlatch.countdown();
            system.err.println("张1捐了一块钱");
        } catch (interruptedexception e) {
            e.printstacktrace();
        }

    });

    executor.execute(() -> {
        try {
            timeunit.milliseconds.sleep(100);
            system.err.println("张2准备捐一块钱");
            countdownlatch.countdown();
            system.err.println("张2捐了一块钱");
        } catch (interruptedexception e) {
            e.printstacktrace();
        }
    });

    executor.execute(() -> {
        try {
            timeunit.milliseconds.sleep(100);
            system.err.println("张3准备捐一块钱");
            countdownlatch.countdown();
            system.err.println("张3捐了一块钱");
        } catch (interruptedexception e) {
            e.printstacktrace();
        }
    });

    system.err.println("我项目启动后,就在这里等人捐钱,不够3块我不走了");
    countdownlatch.await();
    system.err.println("3块钱到手,直接跑路");

    executor.shutdown();
}

结果图:

pic1
这个结果,em,可以看到countdownlatch使用的几个注意点:

  1. 调用countdownlatchawait()方法的线程会阻塞,直到凑够3块钱为止
  2. cyclicbarrier不同,其计完数之后并不会阻塞,而是直接执行接下来的操作
  3. 每次调用countdown()方法都会捐一块钱(计数一次),满了之后调用await()方法的线程不再阻塞

 另外,在上面的代码中,在countdown方法之后还打印信息是为了验证countdown方法不会阻塞当前线程,执行结果不一定如上图那样有顺序的,例如可能出现下方的结果:

pic2
 因为最后一个countdown之后,await所在的线程不再阻塞了,又正好赶上jvm线程调度,所以就会出现上方的结果。

2.countdownlatch的内部实现

  刚才已经讲了countdownlatch的用法,用起来还是不难的。那来看下内部是怎么实现的,又是怎么做到计数之后不跟cyclicbarrier一样阻塞的呢?

  首先来看构造函数吧,countdownlatch只有一个构造函数,如下

public countdownlatch(int count) {
    if (count < 0) throw new illegalargumentexception("count < 0");
    this.sync = new sync(count);
}

 所做的事情也就只有初始化内部对象sync一件事情(校验总不能算一件事吧?),那来看下初始化了个啥玩意

// 变量sync,是不是看起来很眼熟?
private final sync sync;

// 内部类sync,又是一个aqs的产物
private static final class sync extends abstractqueuedsynchronizer {
    private static final long serialversionuid = 4982264981922014374l;

	// 构造方法,就是设置了aqs的state值
    sync(int count) {
        setstate(count);
    }

    int getcount() {
        return getstate();
    }

    /*
     * 可以知道countdownlatch使用的是aqs的共享模式
     * 获取资源方法,正数表示成功,负数表示失败
     */
    protected int tryacquireshared(int acquires) {
        return (getstate() == 0) ? 1 : -1;
    }

    // 释放方法
    protected boolean tryreleaseshared(int releases) {
        for (;;) {
            // state的状态,在countdownlatch中表示剩余计数量,如果为0则表示可以被获取,即await方法不再阻塞
            int c = getstate();
            if (c == 0)
                return false;
            // 本次计数后还剩余的及数量
            int nextc = c-1;
            // cas设置剩余计数量
            if (compareandsetstate(c, nextc))
                // ==0表示锁释放了,之后state的值将一直是0,意思就是之后的await方法都不再阻塞
                return nextc == 0;
        }
    }

 既然涉及到了aqs,那你应该懂我意思了——快去看我写的aqs文章啊

 开个玩笑,我知道各位都多多少少了解一些,上方代码的作用应该知道是干嘛的,不懂也没关系,等下我在下面再讲。

 回到正题,来讲下从上方代码能得到什么信息

1.countdownlatch构造函数count的参数作用就是设置其内部的aqs的状态state,假设count3,那么每次进行countdownaqsstate就减1,减到0的时候await方法就不再阻塞,注意这时候await方法就不再阻塞了,无论你调多少次。

2.countdownlatch里边的sync实现的aqs的共享模式(从tryreleaseshared方法可以看出)

 到这里对其countdownlatch的内部有个差不多印象了,接下来看下其最重要的awaitcountdown方法。

2.1 await方法

public void await() throws interruptedexception {
    sync.acquiresharedinterruptibly(1);
}

 直接调用了aqs的顶级方法,再进去就是aqs的模块了

public final void acquiresharedinterruptibly(int arg)
            throws interruptedexception {
    if (thread.interrupted())
        throw new interruptedexception();
    // 获取资源,成功直接返回,失败执行下方方法(进入同步队列)
    if (tryacquireshared(arg) < 0)
        doacquiresharedinterruptibly(arg);
}

 简单说明一下,这个方法的意思就是调用tryacquireshared的方法尝试获取资源,方法返回负数表示失败返回正数则表示成功失败了则入同步队列(即阻塞),具体的细节可以看下aqs的详解。
 也就是说关键点是 tryacquireshared方法,这个方法刚才在上方已经解释过,这里再放一次。方法逻辑很简单,如果state=0(即计数完毕)则成功,否则失败。

protected int tryacquireshared(int acquires) {
    return (getstate() == 0) ? 1 : -1;
}

  okay,await方法的整个流程大致就是:尝试获取资源,如果失败则阻塞,成功了继续当前线程的操作。什么时候会失败呢,在state!=0的时候,而state这个变量的值我们在构造函数就已经赋予了,需要通过countdown方法来减少。

2.2 countdown

  既然这个方法这么重要,那让它开始它的表演吧。

public void countdown() {
    sync.releaseshared(1);
}

 同样的,直接调用aqs的顶级释放资源的方法。

public final boolean releaseshared(int arg) {
    // 如果资源释放了,那么唤醒同步队列中等待的线程
    if (tryreleaseshared(arg)) {
        // 善后操作
        doreleaseshared();
        return true;
    }
    return false;
}

 关键的方法还是在资源的控制上——tryreleaseshared,代码如下(上方也有):

protected boolean tryreleaseshared(int releases) {
    for (;;) {
        /* 
         * state的状态,在countdownlatch中表示剩余计数量
         * 如果为0则表示可以被获取,即await方法不再阻塞
         */
        int c = getstate();
        // 这里的意思是如果资源已经释放的情况下,就不能再次释放了,释放成功的代码在最后一行
        if (c == 0)
            return false;
        // 本次计数后还剩余的及数量
        int nextc = c-1;
        // cas设置剩余计数量
        if (compareandsetstate(c, nextc))
            // ==0表示锁释放了,之后state的值将一直是0,意思就是之后的await方法都不再阻塞
            return nextc == 0;
    }
}

  到这里countdown方法的迷雾也看清了,每一次调用countdown方法就相当于调用tryreleaseshared方法,如果当前资源还没释放的话,将state-1,判断是否为0如果为0的话表示资源释放唤醒await方法的线程,否则的话只是更新state的值。

 整理一下整个countdownlatch的流程。

1.创建一个countdownlatch,并赋予一个数值,这个值表示需要计数的次数,每次countdown算一次

2.在主线程调用await方法,表示需要计数器完成之前都不能动await方法的内部实现依赖于内部的aqs,调用await方法的时候会尝试去获取资源,成功条件是state=0,也就是说除非countdowncount(构造函数赋予)次之后,才能成功,失败的话当前线程进行休眠

3.在子线程调用countdown方法,每次调用都会使内部的state-1state0的时候资源释放await方法不再阻塞(即使再次调用也是)

3. 小结

  如果理解aqs的话,不止countdownlatch,其他衍生物例如reentrantlock都能轻易的看懂。如果不了解的话也没关系,这篇文章应该能让你对countdownlatch的内部实现有了大概的轮廓。

  简单总结一下,countdownlatch就三个点:构造函数的值、awaitcountdown。构造函数的值表示计数的次数,每次countdown都会使计数减一,减到0的时候await方法所在的线程就不再阻塞。



这篇文章写得,自己都有点不好意思了...

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网