【转】-CountDownLatch详解
CountDownLatch详解
该博客转载自爱宝贝丶 的CountDownLatch详解
1. 简介
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此,在构造CountDownLatch的时候需要传入一个整数n,在这个整数“倒数”到0之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。
2. 使用方法
CountDownLatch主要有两个方法:countDown()和await()。countDown()方法用于使计数器减一,其一般是执行任务的线程调用,await()方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown()方法并没有规定一个线程只能调用一次,当同一个线程调用多次countDown()方法时,每次都会使计数器减一;另外,await()方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行await()方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。如下是其使用示例:
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
Service service = new Service(latch);
Runnable task = () -> service.exec();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task);
thread.start();
}
System.out.println("main thread await. ");
latch.await();
System.out.println("main thread finishes await. ");
}
}
public class Service {
private CountDownLatch latch;
public Service(CountDownLatch latch) {
this.latch = latch;
}
public void exec() {
try {
System.out.println(Thread.currentThread().getName() + " execute task. ");
sleep(2);
System.out.println(Thread.currentThread().getName() + " finished task. ");
} finally {
latch.countDown();
}
}
private void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的例子中,首先声明了一个CountDownLatch对象,并且由主线程创建了5个线程,分别执行任务,在每个任务中,当前线程会休眠2秒。在启动线程之后,主线程调用了CountDownLatch.await()方法,此时,主线程将在此处等待创建的5个线程执行完任务之后才继续往下执行。如下是执行结果:
Thread-0 execute task.
Thread-1 execute task.
Thread-2 execute task.
Thread-3 execute task.
Thread-4 execute task.
main thread await.
Thread-0 finished task.
Thread-4 finished task.
Thread-3 finished task.
Thread-1 finished task.
Thread-2 finished task.
main thread finishes await.
从输出结果可以看出,主线程先启动了五个线程,然后主线程进入等待状态,当这五个线程都执行完任务之后主线程才结束了等待。上述代码中需要注意的是,在执行任务的线程中,使用了try...finally结构,该结构可以保证创建的线程发生异常时CountDownLatch.countDown()方法也会执行,也就保证了主线程不会一直处于等待状态。
3. 使用场景
CountDownLatch非常适合于对任务进行拆分,使其并行执行,比如某个任务执行2s,其对数据的请求可以分为五个部分,那么就可以将这个任务拆分为5个子任务,分别交由五个线程执行,执行完成之后再由主线程进行汇总,此时,总的执行时间将决定于执行最慢的任务,平均来看,还是大大减少了总的执行时间。
另外一种比较合适使用CountDownLatch的地方是使用某些外部链接请求数据的时候,比如图片。在本人所从事的项目中就有类似的情况,因为我们使用的图片服务只提供了获取单个图片的功能,而每次获取图片的时间不等,一般都需要1.5s~2s。当我们需要批量获取图片的时候,比如列表页需要展示一系列的图片,如果使用单个线程顺序获取,那么等待时间将会极长,此时我们就可以使用CountDownLatch对获取图片的操作进行拆分,并行的获取图片,这样也就缩短了总的获取时间。
4. 原理剖析
CountDownLatch是基于AbstractQueuedSynchronizer实现的,在AbstractQueuedSynchronizer中维护了一个volatile类型的整数state,volatile可以保证多线程环境下该变量的修改对每个线程都可见,并且由于该属性为整型,因而对该变量的修改也是原子的。创建一个CountDownLatch对象时,所传入的整数n就会赋值给state属性,当countDown()方法调用时,该线程就会尝试对state减一,而调用await()方法时,当前线程就会判断state属性是否为0,如果为0,则继续往下执行,如果不为0,则使当前线程进入等待状态,直到某个线程将state属性置为0,其就会唤醒在await()方法中等待的线程。如下是countDown()方法的源代码:
public void countDown() {
sync.releaseShared(1);
}
这里sync也即一个继承了AbstractQueuedSynchronizer的类实例,该类是CountDownLatch的一个内部类,其声明如下:
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) {
for (;;) {
int c = getState(); // 获取当前state属性的值
if (c == 0) // 如果state为0,则说明当前计数器已经计数完成,直接返回
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc)) // 使用CAS算法对state进行设置
return nextc == 0; // 设置成功后返回当前是否为最后一个设置state的线程
}
}
}
这里tryReleaseShared(int)方法即对state属性进行减一操作的代码。可以看到,CAS也即compare and set的缩写,jvm会保证该方法的原子性,其会比较state是否为c,如果是则将其设置为nextc(自减1),如果state不为c,则说明有另外的线程在getState()方法和compareAndSetState()方法调用之间对state进行了设置,当前线程也就没有成功设置state属性的值,其会进入下一次循环中,如此往复,直至其成功设置state属性的值,即countDown()方法调用成功。
在countDown()方法中调用的sync.releaseShared(1)调用时实际还是调用的tryReleaseShared(int)方法,如下是releaseShared(int)方法的实现:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
可以看到,在执行sync.releaseShared(1)方法时,其在调用tryReleaseShared(int)方法时会在无限for循环中设置state属性的值,设置成功之后其会根据设置的返回值(此时state已经自减了一),即当前线程是否为将state属性设置为0的线程,来判断是否执行if块中的代码。doReleaseShared()方法主要作用是唤醒调用了await()方法的线程。需要注意的是,如果有多个线程调用了await()方法,这些线程都是以共享的方式等待在await()方法处的,试想,如果以独占的方式等待,那么当计数器减少至零时,就只有一个线程会被唤醒执行await()之后的代码,这显然不符合逻辑。如下是doReleaseShared()方法的实现代码:
private void doReleaseShared() {
for (;;) {
Node h = head; // 记录等待队列中的头结点的线程
if (h != null && h != tail) { // 头结点不为空,且头结点不等于尾节点
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // SIGNAL状态表示当前节点正在等待被唤醒
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 清除当前节点的等待状态
continue;
unparkSuccessor(h); // 唤醒当前节点的下一个节点
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // 如果h还是指向头结点,说明前面这段代码执行过程中没有其他线程对头结点进行过处理
break;
}
}
在doReleaseShared()方法中(始终注意当前方法是最后一个执行countDown()方法的线程执行的),首先判断头结点不为空,且不为尾节点,说明等待队列中有等待唤醒的线程,这里需要说明的是,在等待队列中,头节点中并没有保存正在等待的线程,其只是一个空的Node对象,真正等待的线程是从头节点的下一个节点开始存放的,因而会有对头结点是否等于尾节点的判断。在判断等待队列中有正在等待的线程之后,其会清除头结点的状态信息,并且调用unparkSuccessor(Node)方法唤醒头结点的下一个节点,使其继续往下执行。如下是unparkSuccessor(Node)方法的具体实现:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除当前节点的等待状态
Node s = node.next;
if (s == null || s.waitStatus > 0) { // s的等待状态大于0说明该节点中的线程已经被外部取消等待了
s = null;
// 从队列尾部往前遍历,找到最后一个处于等待状态的节点,用s记录下来
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒离传入节点最近的处于等待状态的节点线程
}
可以看到,unparkSuccessor(Node)方法的作用是唤醒离传入节点最近的一个处于等待状态的线程,使其继续往下执行。前面我们讲到过,等待队列中的线程可能有多个,而调用countDown()方法的线程只唤醒了一个处于等待状态的线程,这里剩下的等待线程是如何被唤醒的呢?其实这些线程是被当前唤醒的线程唤醒的。具体的我们可以看看await()方法的具体执行过程。如下是await()方法的代码:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
await()方法实际还是调用了Sync对象的方法acquireSharedInterruptibly(int)方法,如下是该方法的具体实现:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
可以看到acquireSharedInterruptibly(int)方法判断当前线程是否需要以共享状态获取执行权限,这里tryAcquireShared(int)方法是AbstractQueuedSynchronizer中的一个模板方法,其具体实现在前面的Sync类中,可以看到,其主要是判断state是否为零,如果为零则返回1,表示当前线程不需要进行权限获取,可直接执行后续代码,返回-1则表示当前线程需要进行共享权限。具体的获取执行权限的代码在doAcquireSharedInterruptibly(int)方法中,如下是该方法的具体实现:
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) { // 大于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);
}
}
在doAcquireSharedInterruptibly(int)方法中,首先使用当前线程创建一个共享模式的节点。然后在一个for循环中判断当前线程是否获取到执行权限,如果有(r >= 0判断)则将当前节点设置为头节点,并且唤醒后续处于共享模式的节点;如果没有,则对调用shouldParkAfterFailedAcquire(Node, Node)和parkAndCheckInterrupt()方法使当前线程处于“搁置”状态,该“搁置”状态是由操作系统进行的,这样可以避免该线程无限循环而获取不到执行权限,造成资源浪费,这里也就是线程处于等待状态的位置,也就是说当线程被阻塞的时候就是阻塞在这个位置。当有多个线程调用await()方法而进入等待状态时,这几个线程都将等待在此处。这里回过头来看前面将的countDown()方法,其会唤醒处于等待队列中离头节点最近的一个处于等待状态的线程,也就是说该线程被唤醒之后会继续从这个位置开始往下执行,此时执行到tryAcquireShared(int)方法时,发现r大于0(因为state已经被置为0了),该线程就会调用setHeadAndPropagate(Node, int)方法,并且退出当前循环,也就开始执行awat()方法之后的代码。这里我们看看setHeadAndPropagate(Node, int)方法的具体实现:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
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();
}
}
setHeadAndPropagate(Node, int)方法主要作用是设置当前节点为头结点,并且将唤醒工作往下传递,在传递的过程中,其会判断被传递的节点是否是以共享模式尝试获取执行权限的,如果不是,则传递到该节点处为止(一般情况下,等待队列中都只会都是处于共享模式或者处于独占模式的节点)。也就是说,头结点会依次唤醒后续处于共享状态的节点,这也就是共享锁与独占锁的实现方式。这里doReleaseShared()方法也就是我们前面讲到的会将离头结点最近的一个处于等待状态的节点唤醒的方法。
【转】-CountDownLatch详解的更多相关文章
- CountDownLatch详解
功能描述 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 常见用法 多个人等一个信号后继续执行操作.例如5个运动员,等一个发令员的枪响. 一个人等多个人的信号. ...
- 最强Java并发编程详解:知识点梳理,BAT面试题等
本文原创更多内容可以参考: Java 全栈知识体系.如需转载请说明原处. 知识体系系统性梳理 Java 并发之基础 A. Java进阶 - Java 并发之基础:首先全局的了解并发的知识体系,同时了解 ...
- AQS详解
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- Java并发之AQS详解
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- java并发包java.util.concurrent详解
线程池ThreadPoolExecutor的使用 并发容器之CopyOnWriteArrayList 并发容器之CopyOnWriteArraySet 数据结构之ConcurrentHashMap,区 ...
- (转)Java并发包基石-AQS详解
背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...
- 【1】AQS详解
概述: 它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点加入到同步队列尾部(采用自旋CAS来 ...
- Zookeeper客户端Curator使用详解
Zookeeper客户端Curator使用详解 前提 最近刚好用到了zookeeper,做了一个基于SpringBoot.Curator.Bootstrap写了一个可视化的Web应用: zookeep ...
- Java性能分析之线程栈详解与性能分析
Java性能分析之线程栈详解 Java性能分析迈不过去的一个关键点是线程栈,新的性能班级也讲到了JVM这一块,所以本篇文章对线程栈进行基础知识普及以及如何对线程栈进行性能分析. 基本概念 线程堆栈也称 ...
- Java并发之AQS详解(转)
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
随机推荐
- 【工程应用十】 基于Hessian矩阵的Frangi滤波算法 == 血管图像增强 == Matlab中fibermetric函数的自我实现、加速和优化。
前几天在翻一翻matlab中的帮助文档,无意中发现一个叫fibermetric的图像处理函数,感觉有点意思,可以增强或者说突出一些类似于管状的对象,后面看了下算法的帮助文档,在百度上找了找,原来这也是 ...
- C++ lambda匿名函数
Lambda 表达式完整的格式如下: [捕获列表] (形参列表) mutable 异常列表-> 返回类型 { 函数体 } 各项的含义: 捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以 ...
- Seata源码—8.Seata Saga模式的事务处理
大纲 1.Seata Saga案例简介 2.Seata Saga案例的状态机定义分析 3.Seata Saga分布式事务与状态机关系 4.Seata Saga案例的Dubbo服务调用配置分析 5.Se ...
- 鸿蒙版《智慧农业APP》通过华为云IoT平台实现软件硬件互联
一.原理图 本篇不涉及硬件相关的功能开发,硬件设备使用MQTT客户端模拟,如果有硬件相关经验的可以直接使用真实硬件代替MQTT客户端. 1.华为云物联网服务器 华为云物联网平台是硬件设备端跟移动APP ...
- JXOI2021游记
JXOI2021游记 day0 虽然说退役的概率比较大,但是我不想.唉,这也不是说不想就可以避免的事情呐-- 总之我希望我稳一点就好-- day1 考场上 依照教练说的,先把三道题都看完再动笔. 看T ...
- Visual Studio 17.3
Visual Studio 2022 17.3新UI
- K8s v1.31 新特性:ImageVolume,允许将镜像作为 Volume 进行挂载
本文主要分享一个 K8s 1.31 增加的一个新 Feature:ImageVolume.允许直接将 OCI 镜像作为 Volume 进行挂载,加速 artifact 分发. 1.背景 Kuberne ...
- 大巧不工,袋鼠云正式开源大数据任务调度平台——Taier(太阿)!
2022年2月22日,在今天这个特殊的日子里,历经多年持续迭代和千万周期实例并发调度考验的Taier(太阿)终于开源了! Github开源地址: https://github.com/DTStack/ ...
- 深入浅出了解生成模型-3:Diffusion模型原理以及代码
更加好的排版:https://www.big-yellow-j.top/posts/2025/05/19/DiffusionModel.html 前文已经介绍了VAE以及GAN这里介绍另外一个模型:D ...
- Vertx 实现webapi实战项目(一)
关于Vertx的介绍在官方文档可以查看 https://vertx-china.github.io/docs/.网上也有许多介绍,这里就直接上项目. 一:使用idea搭建gradle项目.添加相关ve ...