Java并发编程系列之CountDownLatch用法及详解

背景

前几天一个同事问我,对这个CountDownLatch有没有了解想问一些问题,当时我一脸懵逼,不知道如何回答。今天赶紧抽空好好补补。不得不说Doug Lea大师真的很牛,设计出如此好的类。

1、回顾旧知识

volatile关键字:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。(这涉及到java内存模型了,有兴趣了解java内存模型的可以先找资料看看)。

2、CountDownLatch简介

CountDownLatch 可以理解就是个计数器,只能减不能加,同时它还有个门闩的作用,当计数器不为0时,门闩是锁着的;当计数器减到0时,门闩就打开了。
如果还不是很理解的话,举个简单的例子就是,你去超市买东西,虽然已经到了关门时间但是只有顾客都走了超市才能关门,至于你买不买东西,超市不关心。只要顾客都走完了,我就可以关门了。

2、CountDownLatch具体使用场景

有A和B两个任务,只有当A任务执行完之后,才能执行B任务。A和B都可以拆分小任务。比如下载一个大文件,可以使用多线程下载,等下载完之后,在统一处理。

2、CountDownLatch实现原理(jdk1.8)

CountDownLatch源码

public class CountDownLatch {
    /**
     * 内部类继承AQS
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

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

        int getCount() {
            return getState();
        }

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

        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;
            }
        }
    }

    private final Sync sync;

    /**
     * 构造方法一般传线程总数
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    /**
     *  等待方法
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

   /**
    * 等待重载超时等待
    */
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    /**
     * 计数减1
     */
    public void countDown() {
        sync.releaseShared(1);
    }

    /**
     *  当前计数
     */
    public long getCount() {
        return sync.getCount();
    }

    /**
    *
     */
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }

为什么定义一个内部类?这种结构的好处在于我们不必关心AbstractQueuedSynchronizer(以下简称AQS)的同步状态管理、线程排队、等待与唤醒等底层操作,我们只需重写我们想要的方法。可生成特定并发工具类。
CountDownLatch主要两个方法就是一是CountDownLatch.await()阻塞当前线程,二是CountDownLatch.countDown()当前线程把计数器减一
看完源码,我们可以看出实现CountDownLatch主要思想就是使用volatile和同步队列来放置这些阻塞队列。
a、CountDownLatch.await()方法
如果让我们自己实现一个await方法我们会怎么做
一、首先会想到是会使用线程间wait/notify,使用synchronized关键字,检查计数器值不为0,然后调用Object.wait();直到计数器值0则调用notifyAll()唤醒等待线程。但是大量的
synchronized代码块会存在假唤醒。
我们还是看看Doug Lea是怎么实现这个类的。

CountDownLatch构造方法

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

构造方法传入了一个int变量,这个int变量是AQS中的state,类型是volatile的,它就是用来表示计数器值的。内存共享这个变量,只有有修改,其他线程都能读取到。

await方法

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

调用await()的方法后,会默认调用sync这个实例的acquireSharedInterruptibly这个方法,并且参数为1,需要注意的是,这个方法声明了一个InterruptedException异常,表示调用该方法的线程支持打断操作。

sync acquireSharedInterruptibly

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

acquireSharedInterruptibly这个方法是sync继承AQS而来的,这个方法的调用是响应线程的打断的,所以在前两行会检查线程是否被打断。接着调用tryAcquireShared()方法来判断返回值,根据值的大小决定是否执行doAcquireSharedInterruptibly()。

tryAcquireShared这个方法是在Sync中重写方法

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

在子类sync的tryAcquireShared中它只验证了计数器的值是否为0,如果为0则返回1,反之返回-1,根据上面代码可以看出,整数就不会执行doAcquireSharedInterruptibly(),该线程就结束方法,继续执行本身代码了。

doAcquireSharedInterruptibly

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);
        }
    }

因为计数器值不为0需要阻塞线程,所以在进入方法时,将该线程包装成节点并加入到同步队列尾部(addWaiter方法),我们看到这个方法退出去的途径直有两个,一个是return,一个是throw InterruptedException。注意最后的finally的处理。return退出方法有必须满足两个条件首先是首节点,其次是计数值为0。
throw InterruptedException是响应打断操作的,线程在阻塞期间,如果你不想在等待了,可以打断线程让它继续运行后面的任务(注意异常处理)。

addWaiter添加节点

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//包装节点
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail; //同步队列尾节点
        if (pred != null) {
            node.prev = pred;//同步队列有尾节点 将我们的节点通过cas方式添加到队列后面
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);// 两种情况执行这个代码 1.队列尾节点为null 2.队列尾节点不为null,但是我们原子添加尾节点失败
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { //  cas形式添加头节点  注意 是头节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;//cas形式添加尾节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;//结束方法必须是尾节点添加成功
                }
            }
        }
    }

b、CountDownLatch.countDown()方法
当部分线程调用await()方法后,它们在同步队列中被挂起,然后循环的检查自己能否满足醒来的条件(还记得那个条件吗?1、state为0,2、该节点为头节点),

 *      +------+  prev +-----+       +-----+
 * head |      | <---- |     | <---- |     |  tail
 *      +------+       +-----+       +-----+

同步队列

volatile Node prev;
volatile Node next;

volatile的prev指向上一个node节点,volatile的next指向下一个node节点。当然如果是头节点,那么它的prev为null,同理尾节点的next为null。

private transient volatile Node head;
private transient volatile Node tail;

用来表示同步队列的头节点和尾节点

countDown方法

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

releaseShared方法

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

在Sync类中并没有releaseShared()方法,所以继承与AQS,看到AQS这个方法中,退出该方法的只有两条路。tryReleaseShared(arg)条件为真执行一个doReleaseShared()退出;条件为假直接退出。

protected boolean tryReleaseShared(int releases) {
    for (;;) {//死循环
        int c = getState();// 获取主存中的state值
        if (c == 0) //state已经为0 直接退出
            return false;
        int nextc = c-1; // 减一 准备cas更新该值
        if (compareAndSetState(c, nextc)) //cas更新
            return nextc == 0; //更新成功 判断是否为0 退出;更新失败则继续for循环,直到线程并发更新成功
    }
}

doReleaseShared方法

private void doReleaseShared() {
    for (;;) {//死循环
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {//如果当前节点是SIGNAL,它正在等待一个信号,或者说它在等待被唤醒,因此做两件事,1是重置waitStatus标志位,2是重置成功后,唤醒下一个节点。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                unparkSuccessor(h);
            }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//如果本身头节点的waitStatus是出于重置状态(waitStatus==0)的,将其设置为“传播”状态。意味着需要将状态向后一个节点传播。
                continue;                
        }
        if (h == head)                   
            break;
    }
}
重点来了

为啥要执行这个方法呀,因为state已经为0啦,我们该将同步队列中的线程状态设置为共享状态(Node.PROPAGATE,默认状态ws == 0),并向后传播,实现状态共享。

退出死循环,只有一条,那就是h==head,即该线程是头节点,且状态为共享状态。

可能有人有疑问,state已经等于0了,我们也通过循环的方式把头节点的状态设置为共享状态,但是它怎么醒过来的呢?看上面doAcquireSharedInterruptibly方法。

在同步队列中挂起的线程,它们自旋的形式查看自己是否满足条件醒来(state==0,且为头节点),如果成立将调用setHeadAndPropagate这个方法

private void setHeadAndPropagate(Node node, int propagate) {
     Node h = head; // Record old head for check below
     setHead(node);
     if (propagate > 0 || h == null || h.waitStatus < 0 ||
         (h = head) == null || h.waitStatus < 0) {
         Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

看一个例子在加深下印象

/**
 * @author shuliangzhao
 * @Title: TestCountDownLatch
 * @ProjectName design-parent
 * @Description: TODO
 * @date 2019/6/2 12:19
 */
public class CountDownLatchExc {

    private static final int i = 2;

    static class MyRunable implements Runnable {

        private int num;

        private CountDownLatch countDownLatch;

        public MyRunable(int num,CountDownLatch countDownLatch) {
            this.num = num;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                System.out.println("第" + num + "个线程开始执行任务...");
                Thread.sleep(2000);
                System.out.println("第" + num + "个线程开始执行结束...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(i);
        for (int i = 0;i < 5;i++) {
            Thread thread = new Thread(new MyRunable(i,countDownLatch));
            thread.start();
        }
        System.out.println("main thread wait.");
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread end...");
    }
}

运行结果


image.png

以上就是CountDownLatch两大重要方法解释,可能理解有偏差,欢迎指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,277评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,777评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,946评论 0 245
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,271评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,636评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,767评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,989评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,733评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,457评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,674评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,155评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,518评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,160评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,114评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,898评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,822评论 2 280
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,705评论 2 273

推荐阅读更多精彩内容