Java并发系列[7]----CountDownLatch源码分析
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。但是CountDownLatch的应用场景却比较广泛,只要你脑洞够大利用它就可以玩出各种花样。最常见的一个应用场景是开启多个线程同时执行某个任务,等到所有任务都执行完再统计汇总结果。下图动态演示了闭锁阻塞线程的整个过程。

上图演示了有5个线程因调用await方法而被阻塞,它们需要等待计数器的值减为0才能继续执行。计数器的初始值在构造闭锁时被指定,后面随着每次countDown方法的调用而减1。下面代码贴出了CountDownLatch的构造方法。
//构造器
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch只有一个带参构造器,必须传入一个大于0的值作为计数器初始值,否则会报错。可以看到在构造方法中只是去new了一个Sync对象并赋值给成员变量sync。和其他同步工具类一样,CountDownLatch的实现依赖于AQS,它是AQS共享模式下的一个应用。CountDownLatch实现了一个内部类Sync并用它去继承AQS,这样就能使用AQS提供的大部分方法了。下面我们就来看一下Sync内部类的代码。
//同步器
private static final class Sync extends AbstractQueuedSynchronizer { //构造器
Sync(int count) {
setState(count);
} //获取当前同步状态
int getCount() {
return getState();
} //尝试获取锁
//返回负数:表示当前线程获取失败
//返回零值:表示当前线程获取成功, 但是后继线程不能再获取了
//返回正数:表示当前线程获取成功, 并且后继线程同样可以获取成功
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
} //尝试释放锁
protected boolean tryReleaseShared(int releases) {
for (;;) {
//获取同步状态
int c = getState();
//如果同步状态为0, 则不能再释放了
if (c == 0) {
return false;
}
//否则的话就将同步状态减1
int nextc = c-1;
//使用CAS方式更新同步状态
if (compareAndSetState(c, nextc)) {
return nextc == 0;
}
}
}
}
可以看到Sync的构造方法会将同步状态的值设置为传入的参数值。之后每次调用countDown方法都会将同步状态的值减1,这也就是计数器的实现原理。在平时使用CountDownLatch工具类时最常用的两个方法就是await方法和countDown方法。调用await方法会阻塞当前线程直到计数器为0,调用countDown方法会将计数器的值减1直到减为0。下面我们来看一下await方法是怎样调用的。
//导致当前线程等待, 直到门闩减少到0, 或者线程被打断
public void await() throws InterruptedException {
//以响应线程中断方式获取
sync.acquireSharedInterruptibly(1);
} //以可中断模式获取锁(共享模式)
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
//首先判断线程是否中断, 如果是则抛出异常
if (Thread.interrupted()) {
throw new InterruptedException();
}
//1.尝试去获取锁
if (tryAcquireShared(arg) < 0) {
//2. 如果获取失败则进人该方法
doAcquireSharedInterruptibly(arg);
}
}
当线程调用await方法时其实是调用到了AQS的acquireSharedInterruptibly方法,该方法是以响应线程中断的方式来获取锁的,上面同样贴出了该方法的代码。我们可以看到在acquireSharedInterruptibly方法首先会去调用tryAcquireShared方法尝试获取锁。我们看到Sync里面重写的tryAcquireShared方法的逻辑,方法的实现逻辑很简单,就是判断当前同步状态是否为0,如果为0则返回1表明可以获取锁,否则返回-1表示不能获取锁。如果tryAcquireShared方法返回1则线程能够不必等待而继续执行,如果返回-1那么后续就会去调用doAcquireSharedInterruptibly方法让线程进入到同步队列里面等待。这就是调用await方法会阻塞当前线程的原理,下面看看countDown方法是怎样将阻塞的线程唤醒的。
//减少门闩的方法
public void countDown() {
sync.releaseShared(1);
} //释放锁的操作(共享模式)
public final boolean releaseShared(int arg) {
//1.尝试去释放锁
if (tryReleaseShared(arg)) {
//2.如果释放成功就唤醒其他线程
doReleaseShared();
return true;
}
return false;
}
可以看到countDown方法里面调用了releaseShared方法,该方法同样是AQS里面的方法,我们在上面也贴出了它的代码。releaseShared方法里面首先是调用tryReleaseShared方法尝试释放锁,tryReleaseShared方法在AQS里面是一个抽象方法,它的具体实现逻辑在子类Sync类里面,我们在上面贴出的Sync类代码里可以找到该方法。tryReleaseShared方法如果返回true表示释放成功,返回false表示释放失败,只有当将同步状态减1后该同步状态恰好为0时才会返回true,其他情况都是返回false。那么当tryReleaseShared返回true之后就会马上调用doReleaseShared方法去唤醒同步队列的所有线程。这样就解释了为什么最后一次调用countDown方法将计数器减为0后就会唤醒所有被阻塞的线程。CountDownLatch基本的原理大致就是这些,下面我们看一个它的使用示例。
应用场景:在玩欢乐斗地主时必须等待三个玩家都到齐才可以进行发牌。
public class Player extends Thread {
private static int count = 1;
private final int id = count++;
private CountDownLatch latch;
public Player(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println("【玩家" + id + "】已入场");
latch.countDown();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
System.out.println("牌局开始, 等待玩家入场...");
new Player(latch).start();
new Player(latch).start();
new Player(latch).start();
latch.await();
System.out.println("玩家已到齐, 开始发牌...");
}
}

运行结果显示发牌操作一定是在所有玩家都入场后才进行。我们将23行的latch.await()注释掉,对比下看看结果。

可以看到在注释掉latch.await()这行之后,就不能保证在所有玩家入场后才开始发牌了。
Java并发系列[7]----CountDownLatch源码分析的更多相关文章
- Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式
在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...
- Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式
通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...
- Java并发系列[5]----ReentrantLock源码分析
在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...
- Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析
学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, Cyc ...
- Java并发系列[4]----AbstractQueuedSynchronizer源码分析之条件队列
通过前面三篇的分析,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排 ...
- Java并发系列[6]----Semaphore源码分析
Semaphore(信号量)是JUC包中比较常用到的一个类,它是AQS共享模式的一个应用,可以允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制.Semapho ...
- Java并发系列[9]----ConcurrentHashMap源码分析
我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashM ...
- Java并发系列[8]----CyclicBarrier源码分析
现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始.例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始.在JUC包中为我们 ...
- Java并发系列[10]----ThreadPoolExecutor源码分析
在日常的开发调试中,我们经常会直接new一个Thread对象来执行某个任务.这种方式在任务数较少的情况下比较简单实用,但是在并发量较大的场景中却有着致命的缺陷.例如在访问量巨大的网站中,如果每个请求都 ...
随机推荐
- 洛谷 [P1169] [ZJOI2007] 最大的正方形
本题是一道求最大子矩阵的题,可以使用悬线法来做,因为是相邻的01矩阵,所以需要对悬线法进行改动. #include <iostream> #include <cstdio> # ...
- ZOJ1450 BZOJ1136 BZOJ1137 HDU3932[最小圆覆盖]
Minimal Circle Time Limit: 5 Seconds Memory Limit: 32768 KB You are to write a program to find ...
- 自动化测试selenium(四)check,选中复选框,操作一组元素
定位复选框位置 打开浏览器,按F12,审查元素 接下来,我们要实现选中复选框 List<WebElement> inputs = driver.findElements(By.tagNam ...
- Java数据结构和算法(九)——高级排序
春晚好看吗?不存在的!!! 在Java数据结构和算法(三)——冒泡.选择.插入排序算法中我们介绍了三种简单的排序算法,它们的时间复杂度大O表示法都是O(N2),如果数据量少,我们还能忍受,但是数据量大 ...
- DaemonSet 案例分析 - 每天5分钟玩转 Docker 容器技术(130)
本节详细分析两个 k8s 自己的 DaemonSet:kube-flannel-ds 和 kube-proxy . kube-flannel-ds 下面我们通过分析 kube-flannel-ds 来 ...
- Java实现单链表的快速排序和归并排序
本文描述了LeetCode 148题 sort-list 的解法. 题目描述如下: Sort a linked list in O(n log n) time using constant space ...
- java递归
package com.sun.test; import java.util.ArrayList; import java.util.List; /** * 递归 * */ public class ...
- PPPoE拨号流程
PPPoE(Point to Point Protocol over Ethernet,基于以太网的点对点协议)的工作流程包含发现(Discovery)和会话(Session)两个阶段,发现阶段是无状 ...
- C语言学习之插入排序
此前的一些博文分别写了C语言中经典的排序方式,选择排序 冒泡排序 桶排序,此文就写 插入排序吧. 相对于冒泡排序,插入排序就比较方便快捷了.和冒泡 选择排序一样,插入排序也需要比较大小.可以这样理解插 ...
- MongoDB 搭建可复制群集
一.概述 MongoDB复制群集支持节点故障自动切换,最小配置应包含3个节点,正常情况下应该至少包含两个数据节点,第三个节点可以是数据节点也可以是仲裁节点.仲裁节点的作用是当出现偶数节点导致无法仲裁的 ...