多线程编程之 CountDownLatch

CountDownLatch 是什么?

CountDownLatch 一般称为闭锁计数器,是一种多线程同步工具,属于 AQS 体系的一员。

常用于让协调线程等待一组工作线程全部“完成工作“或“满足特定条件"后继续进行下去。

但其实也可以和 CyclicBarrier让一组线程全部到达指定点后才继续执行,不过不如 CyclicBarrier简单且不可重用,所以一般一组线程自等待的场景我们倾向于直接使用 CyclicBarrier

CountDownLatch 怎么用?

老板有个保险箱,为了保证安全,指定 3 个亲信拿着密码。只有当 3 个亲信同时输入密码时,老板才能打开保险箱。

哪天老板说要取钱跑路了,赶紧开锁。所以亲信赶紧输入密码。

多线程编程之 CountDownLatch_第1张图片

简单方式

我们可以用让老板用 int 做计数器,一直循环等待完成:

private static void rr() {
    System.out.println("老板:我要开锁");
    AtomicInteger count = new AtomicInteger(3);
    for (int i = 0; i < count.get(); i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                count.decrementAndGet();
            }
        }).start();
    }
    while(count.get() != 0) {
        System.out.println("老板:几个傻子给我快点");
        Thread.sleep(1000);
    }
    System.out.println("老板:拿钱跑路, 亲信卖了");
}

但是呢,会有几个问题?

  • 简单的 int 会有并发问题,我们要么用 volatile 修饰,要么使用原子类

  • 老板一直催催催,很烦的,需要让他安静一下,就要让他睡一会。

    可是保险箱的锁在第一次输入后,就要在限定时间内全部完成并打开,那老板睡过了怎么办。

    是不是还需要其他的线程通信手段呢?

协调线程等待工作线程

亲信 1:报告老板,咱发现一个好东西。既可以安全的输入密码,几个人不会错乱

亲信 2:全部输入之后,马上就通知您了。

亲信 3:还支持限定时间内不完成,也会通知到您出问题了。(OS:指不定我们哪个被人给“灭口”了)。

老板:好东西,搞出来给我看看。

public class CountDownLatchDemo {
    public static void main(String[] args) {
        System.out.println("老板:我要开锁");
        // 指定 3 个亲信
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            new Thread(new Follower(latch)).start();
        }
        try {
            latch.await(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            System.out.println("老板:都是傻子吗,这都输入失败了");
        }
        System.out.println("老板:拿钱跑路, 亲信卖了");
    }

    private static class Follower implements Runnable {
        private CountDownLatch latch;
        public Follower(CountDownLatch latch) {
            this.latch = latch;
        }
        @Override
        public void run() {
            System.out.println("亲信:输入密码ing");
            latch.countDown();
            System.out.println("亲信:报告老板, 输入完成");
        }
    }
}
老板:我要开锁
亲信:输入密码ing
亲信:报告老板, 输入完成
亲信:输入密码ing
亲信:报告老板, 输入完成
亲信:输入密码ing
亲信:报告老板, 输入完成
老板:拿钱跑路, 亲信卖了
相互等待

苦于老板的压榨,亲信们走上了截然不同的道路。

亲信 OS:老板终究会把我们卖了,我们自己翻身做主人吧,把钱给拿了。

多线程编程之 CountDownLatch_第2张图片

private static void cycleCountDown() throws InterruptedException {
    System.out.println("亲信们:不成功便成仁!!!!");
    CountDownLatch latch = new CountDownLatch(3);
    for (int i = 0; i < 3; i++) {
        new Thread(new MyFollower(latch)).start();
    }
    Thread.sleep(1000);
}

private static class MyFollower implements Runnable {
    private CountDownLatch latch;

    public MyFollower(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("亲信:输入密码ing");
        latch.countDown();
        System.out.println("亲信:我这边搞定了,等你们.");
        try {
            latch.await(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            System.out.println("OS:你们还想不想要钱了");
        }

        System.out.println("亲信:成了!!!!!");
    }
}
亲信们:不成功便成仁!!!!!
亲信:输入密码ing
亲信:输入密码ing
亲信:我这边搞定了,等你们.
亲信:我这边搞定了,等你们.
亲信:输入密码ing
亲信:我这边搞定了,等你们.
亲信:成了!!!!!
亲信:成了!!!!!
亲信:成了!!!!!
CountDownLatch 怎么实现?

主要方法如下,非常简洁:

多线程编程之 CountDownLatch_第3张图片

最最关键的还是 Sync的内部类,实现了 AbstractQueuedSynchronizer(AQS)。

这里不再说明 AQS 的内容,会简单提一下,深入研究参见:AbstractQueuedSynchronizer(AQS):并发工具的基石 (juejin.cn)

CountDownLatch 内部会持有一个 Sync的实例:private final Sync sync;

在构造时指定数量的同时,去构建这个 Sync

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

Sync 接受到这个计数后,那了解 AQS 的同学马上就会明白,这是要赋予 state 这个字段真实的含义:剩余需达到指定条件的数量。

private static final class Sync extends AbstractQueuedSynchronizer {

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }
}
怎么让老板等着,不比比叨叨?

await 有两个重载方法:

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

这里就是 AQS 的范畴了:

多线程编程之 CountDownLatch_第4张图片

而前一个步骤是 Sync 需要实现的方法。

因为需要对 state 这个计数值进行判断,但是这个计数是在子类才赋予的含义,所以操作方法也在子类:

只有计数 state = 0 才能继续进行。

 private static final class Sync extends AbstractQueuedSynchronizer {
     protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }
 }

那如果没到 0,就让老板等着。AQS 内部维护了一个等待资源的队列,延伸到子类,那就是等待 getState() == 0 的情况发生。

多线程编程之 CountDownLatch_第5张图片

亲信:既然老板不比比了,就要赶紧输入密码,不然超时又要吵了。

亲信 --;亲信 --;亲信–。完成!!!

输入密码,通知老板
public void countDown() {
    sync.releaseShared(1);
}

看看,看看,又是 AQS:

image-20210618165537524

上面说到,如果是简单的 int,容易出现并发问题,导致输入混乱,那最后还得挨老板批。

所以需要可靠的方式:CAS + volatile(state 本质是 volatile 修饰的)

private static final class Sync extends AbstractQueuedSynchronizer {
    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))
                // 恰好完成,返回 true,以便进行通知。
                return nextc == 0;
        }
    }
}

返回 true,就可以唤醒老板了:老板老板,输入完成!!!

多线程编程之 CountDownLatch_第6张图片

总结

其实省略了很多 AQS 的内容,这东西复杂也挺复杂,搞明白也挺好明白。一言两语说不清,不如看看这两篇文章

  • AbstractQueuedSynchronizer(AQS):并发工具的基石 (juejin.cn)
  • AQS Condition 源码解析 (juejin.cn)

可是还有问题,这玩意是一次性的。

平行时空 1:

老板:这玩意只能用一次有个屁用,我这是保险箱,不是射火箭!!!

CountDownLatch:射火箭我也可以的,毕竟火箭发射掌握在几个总工程师手上呢~

亲信:

平行时空 2:

亲信们:这玩意咋坏了,怎么让老板短期不发现啊

CyclicBarrier:听说有人在 cue 我?

你可能感兴趣的:(java,Java,源码,java,并发编程)