上一篇:Java中的阻塞队列 BlockingQueue 详解

本文目录

1、CountDownLatch的基本概述

2、CountDownLatch的使用案例

3、CountDownLatch的源码分析


1、CountDownLatch的基本概述

CountDownLatch允许一个或多个线程等待其他线程完成操作。

CountDownLatch又称为“闭锁”,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到等待的线程操作完成或者等待超时,自己再开始执行。

CountDownLatch可以延迟线程的进度直到其他线程到达终止状态,它可以用来确保某些任务在其他任务都完成后再继续进行:

  • 确保某个计算在其需要的所有资源都被初始化之后再继续执行;
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动;
  • 等待直到某个操作所有参与者都准备就绪后再继续执行。

另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。

这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。但是CountDownLatch的应用场景却比较广泛,只要你脑洞够大利用它就可以玩出各种花样。最常见的一个应用场景是开启多个线程同时执行某个任务,等到所有任务都执行完再统计汇总结果。下图动态演示了闭锁阻塞线程的整个过程。

上图演示了有5个线程因调用await方法而被阻塞,它们需要等待计数器的值减为0才能继续执行。计数器的初始值在构造闭锁时被指定,后面随着每次countDown方法的调用而减1。


2、CountDownLatch的使用案例

【案例1】

我们看下 Doug Lea 在 java doc 中给出的例子,这个例子非常实用,我们经常会写这个代码。

假设我们有 N ( N > 0 ) 个任务,那么我们会用 N 来初始化一个 CountDownLatch,然后将这个 latch 的引用传递到各个线程中,在每个线程完成了任务后,调用 latch.countDown() 代表完成了一个任务。调用 latch.await() 的方法的线程会阻塞,直到所有的任务完成。

package com.zju.CountDownLatch;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;public class Driver {public static void main(String[] args) throws InterruptedException {CountDownLatch doneSignal = new CountDownLatch(10);Executor e = Executors.newFixedThreadPool(4);// 创建10个任务,提交给线程池来执行for (int i = 0; i < 10; i++) {e.execute(new WorkerRunnable(doneSignal, i));}// 等待所有的任务都完成了,这个方法才会返回doneSignal.await();}
}class WorkerRunnable implements Runnable{private final CountDownLatch doneSignal;private final int i;WorkerRunnable(CountDownLatch doneSignal, int i){this.doneSignal = doneSignal;this.i = i;}@Overridepublic void run() {try {doWork(i);// 这个线程的任务完成了,调用 countDown 方法doneSignal.countDown();} catch (Exception e) {}}public void doWork(int i){System.out.println(i);}
}

所以说 CountDownLatch 非常实用,我们常常会将一个比较大的任务进行拆分,然后开启多个线程来执行,等所有线程都执行完了以后,再往下执行其他操作。这里例子中,只有 main 线程调用了 await 方法

我们再来看另一个例子,这个例子很典型,用了两个 CountDownLatch:

class Driver { void main() throws InterruptedException {CountDownLatch startSignal = new CountDownLatch(1);CountDownLatch doneSignal = new CountDownLatch(N);for (int i = 0; i < N; ++i) new Thread(new Worker(startSignal, doneSignal)).start();// 这边插入一些代码,确保上面的每个线程先启动起来,才执行下面的代码。doSomethingElse();            // don't let run yet// 因为这里 N == 1,所以,只要调用一次,那么所有的 await 方法都可以通过startSignal.countDown();      doSomethingElse();// 等待所有任务结束doneSignal.await();           }
}class Worker implements Runnable {private final CountDownLatch startSignal;private final CountDownLatch doneSignal;Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {this.startSignal = startSignal;this.doneSignal = doneSignal;}public void run() {try {// 为了让所有线程同时开始任务,我们让所有线程先阻塞在这里// 等大家都准备好了,再打开这个门栓startSignal.await();doWork();doneSignal.countDown();} catch (InterruptedException ex) {} }void doWork() { ...}
}

这个例子中,doneSignal 同第一个例子的使用,我们说说这里的 startSignal。N 个新开启的线程都调用了startSignal.await() 进行阻塞等待,它们阻塞在栅栏上,只有当条件满足的时候(startSignal.countDown()),它们才能同时通过这个栅栏。

如果始终只有一个线程调用 await 方法等待任务完成,那么 CountDownLatch 就会简单很多,所以之后的源码分析读者一定要在脑海中构建出这么一个场景:有 m 个线程是做任务的,有 n 个线程在某个栅栏上等待这 m 个线程做完任务,直到所有 m 个任务完成后,n 个线程同时通过栅栏。

【案例2】

应用场景:在玩欢乐斗地主时必须等待三个玩家都到齐才可以进行发牌。

package com.zju.CountDownLatch;import java.util.concurrent.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;}@Overridepublic 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()这行之后,就不能保证在所有玩家入场后才开始发牌了。


3、CountDownLatch的源码分析

因为CountDownLatch的源码比较少,这里直接全部贴出来,方便直观感受下CountDownLatch的内部结构。可以发现CountDownLactch是基于AQS共享式锁基础上实现的。

public class 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();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));}public void countDown() {sync.releaseShared(1);}public long getCount() {return sync.getCount();}public String toString() {return super.toString() + "[Count = " + sync.getCount() + "]";}
}

首先来看下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;}// 否则的话就将同步状态减1int 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后就会唤醒所有被阻塞的线程。

private void doReleaseShared() {for (;;) {Node h = head;    // 设置头节点为hif (h != null && h != tail) {int ws = h.waitStatus;   // 获取头节点的同步状态if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;    // 如果CAS失败,就不停的尝试        unparkSuccessor(h);  // 唤醒其他节点}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                }if (h == head)                   break;}
}

全文完!

上一篇:Java中的阻塞队列 BlockingQueue 详解


推荐两篇文章:

1、Java并发系列 | CountDownLatch源码分析【本文源码分析部分参考于此】

2、AQS共享模式与并发工具类的实现

【搞定Java并发编程】第24篇:Java中的并发工具类之CountDownLatch相关推荐

  1. java io 并发编程,JAVA进阶系列 - 并发编程 - 第1篇:进程线程并发并行

    学习目标进程 线程 并发 并行 内容 一.进程与线程 "专业"点的说法就是:进程是资源分配的最小单位,线程是CPU调度的最小单位. 大哥,我错了,别打脸! 进程 线程就是用来加载指 ...

  2. java并发编程的艺术和并发编程这一篇就够了

    java并发编程的艺术(精华提炼) 通常我们在使用编发编程时,主要目的是为了程序能够更快的处理,但是并不是说更多的线程就一定能够让程序变得足够快,有时候太多的线程反而消耗了更多的资源,反而让程序执行得 ...

  3. Java并发编程 - 第十一章 Java并发编程实践

    前言: 当你在进行并发编程时,看着程序的执行速度在自己的优化下运行得越来越快,你会觉得越来越有成就感,这就是并发编程的魅力.但与此同时,并发编程产生的问题和风险可能也会随之而来.本章先介绍几个并发编程 ...

  4. 【面试锦囊】14种模式搞定面试算法编程题(8-14)

    面试锦囊之知识整理系列 面试锦囊系列一直有收到大家的反馈,包括后台内推成功的消息.朋友的同事从创业小公司成功跳到huawei等等,非常高兴小破号的这些整理分享能够真正地帮助到大家,以后也会继续.为了更 ...

  5. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

  6. 并发编程之深入理解java线程

    并发编程之深入理解java线程 一.线程基础知识 1.1 进程和线程 1.1.1 进程 1.1.2 线程 1.1.3 进程与线程的区别 1.1.4 进程间通信的方式 1.2 线程的同步互斥 1.3 上 ...

  7. 并发编程:我对Java并发编程的总结和思考

    编写优质的并发代码是一件难度极高的事情.Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更 ...

  8. 阿里、字节面试必撸,阿里大能总结 410 页 Java 并发编程手册全彩版,附录高并发面试真题及答案详解

    虽然说并发编程的第一原则是不要写并发程序.但是,随着硬件的驱动和国内互联网行业的飞速发展,对软件系统的并发量要求越来越高,传统的中间件和数据库已经成为性能的瓶颈.并发编程已经成为绕不开的话题,也慢慢成 ...

  9. 实战并发编程 - 04基于不可变模式解决并发问题_2

    文章目录 Pre 业务描述 短信服务商基本信息 短信路由网关 基于不可变模式改造代码 第一步先将SmsInfo改造为不可变对象 接着在需要将获取服务商列表的代码改造为防御性复制 接着提供一个直接替换S ...

  10. java编程中的断言工具类(org.springframework.util.Assert)

    转自:https://blog.csdn.net/gokeiryou263/article/details/19612471 断言工具类:Assert类, java.lang.Object ---&g ...

最新文章

  1. XXL-JOB v2.0.2,分布式任务调度平台 | 多项特性优化更新
  2. java 协程线程的区别_为什么 Java 坚持多线程不选择协程?
  3. 洛谷P1396 营救 题解
  4. 按钮不通过表单连接servlet_JavaWeb之Servlet(一)
  5. struts2多文件动态下载及中文解决方案
  6. html cookie传参,页面间固定参数,通过cookie传值的实现方法
  7. C++:不同数据类型作为参数传递和作为返回值的例子
  8. 雷凌linux车机升级_绿老师学堂:15万合资车谁更“聪明”?体验思域/福克斯/雷凌车机...
  9. java web 对cookie技术、session技术进行小结
  10. 机器学习笔记(二十五):支撑向量机(SVM)
  11. 《软件体系结构》 练习题
  12. 计算机操作系统知识整理-计算机操作系统概述(计算机操作系统入门指南)
  13. NYOJ----366D的小L
  14. linux命令日志抓取,linux抓取某条日志记录的命令
  15. 【论文】mac系统下的citespace与使用
  16. 瑞禧整理常见的抗体药物偶连物(ADC-Linker)名称及结构式大全
  17. 蓝桥杯CT107D:关于矩阵键盘工作原理及其应用
  18. Unity3d办公场景灯光布设与光影烘焙及后处理【2020】
  19. pdf文件去掉广告,水印,背景和删除密码方法收藏
  20. java websphere_将Java Web 应用部署至 WebSphere 7

热门文章

  1. 精品软件推荐 CCleaner中文版 好用的系统垃圾清理工具
  2. Linux系统InfluxDB数据和日志目录迁移教程
  3. 皮尔森相关性系数的计算python代码(三)
  4. 如何修改数组对象的属性名(把key替换成想要的key,值不变)
  5. bootstrap模态框保存后清除模态框数据的方法
  6. 优秀网页翻译:高精度 10MHz GPS 驯服钟 (GPSDO) - Part 4
  7. python怎么样?
  8. Oracle数据库下的DDL、DML、DQL、TCL、DCL
  9. c语言算法有效性,BerForest—C语言学习笔记-《算法》
  10. 跨境电商 Shopee 的实时数仓之路