这篇文章主要讲解java中一个比较常用的同步工具类CountDownLatch,不管是在工作还是面试中都比较常见。我们将通过案例来进行讲解分析。

这个是我重新又看了一遍,对于CountDownLatch,美团二面的时候、京东一面、腾讯二面都问到了,问的方式又出奇的类似,基本上就是通过场景,而不是直接问。

一、定义

CountDownLatch的作用很简单,就是一个或者一组线程在开始执行操作之前,必须要等到其他线程执行完才可以。我们举一个例子来说明,在考试的时候,老师必须要等到所有人交了试卷才可以走。此时老师就相当于等待线程,而学生就好比是执行的线程。

注意:java中还有一个同步工具类叫做CyclicBarrier,他的作用和CountDownLatch类似。同样是等待其他线程都完成了,才可以进行下一步操作,我们再举一个例子,在打王者的时候,在开局前所有人都必须要加载到100%才可以进入。否则所有玩家都相互等待。我们看一下区别:

CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行

CyclicBarrier  : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

关键点其实就在于那N个线程

(1)CountDownLatch里面N个线程就是学生,学生做完了试卷就可以走了,不用等待其他的学生是否完成

(2)CyclicBarrier  里面N个线程就是所有的游戏玩家,一个游戏玩家加载到100%还不可以,必须要等到其他的游戏玩家都加载到100%才可以开局

现在应该理解CountDownLatch的含义了吧,下面我们使用一个代码案例来解释。

二、使用

我们使用学生考试的案例来进行演示:1public class CountDownLatchTest{

2    static CountDownLatch countDownLatch = new CountDownLatch(2);
3    public static void main(String[] args){
4        System.out.println("全班同学开始考试:一共两个学生");
5        new Thread(() -> {
6            System.out.println("第一个学生交卷,countDownLatch减1");
7            countDownLatch.countDown();
8        }).start();
9        new Thread(() -> {
10            System.out.println("第二个学生交卷,countDownLatch减1");
11            countDownLatch.countDown();
12        }).start();
13        try {
14            countDownLatch.await();
15        } catch (InterruptedException e) {
16            e.printStackTrace();
17        }
18        System.out.println("老师清点试卷,在此之前,只要一个学生没交,"
19                + "countDownLatch不为0,不能离开考场");
20    }
21}
在上面,我们定义了一个CountDownLatch,并设置其值为2。有两个学生使用两个线程来表示,然后依次执行。最后老师线程(main线程)在学生线程都执行完了才可以执行。我们来运行一边看看结果。
现在我们应该能体会到其用法了吧。在上面我们的等待线程时老师(main线程)。
下面我们对这个countDownLatch分析一下。为什么具有上面的特点。
三、原理
在上面我们看到,CountDownLatch主要使用countDown方法进行减1操作,使用await方法进行等到操作。我们进入到源码中看看。本源码基于jdk1.8。特在此说明。
1、countDown原理1    /**
2     * Decrements the count of the latch, releasing all waiting threads if
3     * the count reaches zero.
4     *
5     * 
If the current count is greater than zero then it is decremented.
6     * If the new count is zero then all waiting threads are re-enabled for
7     * thread scheduling purposes.
8     *
9     * 
If the current count equals zero then nothing happens.
10     */
11    public void countDown(){
12        sync.releaseShared(1);
13    }
英语不好的人看起来真的是一脸懵逼,不过信号上面的英语还都是简单的英语,大致意思是这样的:CountDownLatch里面保存了一个count值,通过减1操作,直到为0时候,等待线程才可以执行。而且通过源码也可以看到这个countDown方法其实是通过sync调用releaseShared(1)来完成的。
OK。到了这一步我们可能会纳闷,sync是个什么鬼,releaseShared方法又是如何实现的。我们不妨接着看源码,在CountDownLatch的开头我们找到了答案,原来这个sync在这里定义了。1    private static final class Sync extends AbstractQueuedSynchronizer{
2        private static final long serialVersionUID = 4982264981922014374L;
3        Sync(int count) {
4            setState(count);
5        }
6        int getCount(){
7            return getState();
8        }
9        protected int tryAcquireShared(int acquires){
10            return (getState() == 0) ? 1 : -1;
11        }
12        protected boolean tryReleaseShared(int releases){
13            // Decrement count; signal when transition to zero
14            for (;;) {
15                int c = getState();
16                if (c == 0)
17                    return false;
18                int nextc = c-1;
19                if (compareAndSetState(c, nextc))
20                    return nextc == 0;
21            }
22        }
23    }
在这里我们发现继承了AbstractQueuedSynchronizer(AQS)。AQS的其中一个作用就是维护线程状态和获取释放锁。在这里也就是说CountDownLatch使用AQS机制维护锁状态。而releaseShared(1)方法就是释放了一个共享锁。
现在理解了吧,底层使用AQS机制调用releaseShared方法释放一个锁资源。那么等待的方法是如何实现的呢?
2、await原理1   public void await() throws InterruptedException{
2        sync.acquireSharedInterruptibly(1);
3    }
4
5    public boolean await(long timeout, TimeUnit unit)
6        throws InterruptedException{
7        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
8    }
这俩方法都是让线程等待,第一个没有实现限制,第二个有时间限制,我们一个一个来看。
(1)await()
await()底层主要是acquireSharedInterruptibly方法实现的,继续跟进去看看。1    public final void acquireSharedInterruptibly(int arg)
2            throws InterruptedException{
3        if (Thread.interrupted())
4            throw new InterruptedException();
5        if (tryAcquireShared(arg) 
6            doAcquireSharedInterruptibly(arg);
7    }
这里面有两个if语句,首先第一个判断是否被中断,如果被中断了,那就抛出中断异常。然后判断当前是否还有线程未执行,如果有那就,那就执行doAcquireSharedInterruptibly方法继续等待。1//这是AQS里面的方法
2//arg在这里调用的是1,表示countDown是否减少到了0
3//如果到0了,那说明满足了要求,返回1,不再等待
4//如果没有达到0,说明还有线程未执行,必须要等到所有的线程
5//执行结束才可以,返回-1,此时小于0,执行doAcquireSharedInterruptibly
6protected int tryAcquireShared(int arg){
7     throw new UnsupportedOperationException();
8}
上面函数的意思已经在注释里面了,下面我们就来看看这个doAcquireSharedInterruptibly是如何实现的。1  private void doAcquireSharedInterruptibly(int arg)
2        throws InterruptedException{
3        final Node node = addWaiter(Node.SHARED);
4        boolean failed = true;
5        try {
6            for (;;) {
7                final Node p = node.predecessor();
8                if (p == head) {
9                    int r = tryAcquireShared(arg);
10                    if (r >= 0) {
11                        setHeadAndPropagate(node, r);
12                        p.next = null; // help GC
13                        failed = false;
14                        return;
15                    }
16                }
17                if (shouldParkAfterFailedAcquire(p, node) &&
18                    parkAndCheckInterrupt())
19                    throw new InterruptedException();
20            }
21        } finally {
22            if (failed)
23                cancelAcquire(node);
24        }
25    }
这块的代码比较长,不过大致意思我可以描述一下,他会用一个一个的节点将线程串起来 等达到条件后再一个一个的唤醒。核心就是第三行的addWaiter函数。我们可以再跟进去看看吧。1   private Node addWaiter(Node mode){
2        Node node = new Node(Thread.currentThread(), mode);
3        // Try the fast path of enq; backup to full enq on failure
4        Node pred = tail;
5        if (pred != null) {
6            node.prev = pred;
7            if (compareAndSetTail(pred, node)) {
8                pred.next = node;
9                return node;
10            }
11        }
12        enq(node);
13        return node;
14    }
你会发现这里面也使用了CAS机制。而且就是使用链表穿起来的。
(2) await(long timeout, TimeUnit unit)
这个方法的意思是等待指定的时间,如果还有线程没执行完,那就接着执行。就好比考完试了,还有同学没交试卷,此时因为到时间了。不管三七二十一也不管剩下的同学是否提交,直接就走了。其底层是通过Sync的tryAcquireSharedNanos方法实现的,我们接着进入到源码中看看。1   public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
2            throws InterruptedException{
3        if (Thread.interrupted())
4            throw new InterruptedException();
5        return tryAcquireShared(arg) >= 0 ||
6            doAcquireSharedNanos(arg, nanosTimeout);
7    }
在这里皮球又一次被踢走了,真正实现的其实就是doAcquireSharedNanos方法,tryAcquireShared方法主要是判断是否当前满足wait的条件。我们接着看。1   private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
2            throws InterruptedException{
3        if (nanosTimeout <= 0L)
4            return false;
5        final long deadline = System.nanoTime() + nanosTimeout;
6        final Node node = addWaiter(Node.SHARED);
7        boolean failed = true;
8        try {
9            for (;;) {
10                final Node p = node.predecessor();
11                if (p == head) {
12                    int r = tryAcquireShared(arg);
13                    if (r >= 0) {
14                        setHeadAndPropagate(node, r);
15                        p.next = null; // help GC
16                        failed = false;
17                        return true;
18                    }
19                }
20                nanosTimeout = deadline - System.nanoTime();
21                if (nanosTimeout <= 0L)
22                    return false;
23                if (shouldParkAfterFailedAcquire(p, node) &&
24                    nanosTimeout > spinForTimeoutThreshold)
25                    LockSupport.parkNanos(this, nanosTimeout);
26                if (Thread.interrupted())
27                    throw new InterruptedException();
28            }
29        } finally {
30            if (failed)
31                cancelAcquire(node);
32        }
33    }

上面的代码看似长,最核心的就是for循环里面的,最主要的意思就是如果当前还有线程未执行而且过了超时时间,那就直接执行等待线程就好了,不再等了。也就是我在指定的时间内你没执行完我等着你,要是超了这个时间点我就不管了。

对于CountDownLatch来说原理主要还是通过源码来认识。不过CountDownLatch看起来虽然很好用,也有很多不足之处,比如说CountDownLatch是一次性的 , 计数器的值只能在构造方法中初始化一次 , 之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后 , 它不能再次被使用。

OK。对其介绍就先到这里吧。