这篇文章主要讲解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。对其介绍就先到这里吧。