一、概述  

  CountDownLatch是JAVA提供在java.util.concurrent包下的一个辅助类,指定的一个或多个线程等待其他线程执行完成后执行。

  能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

  闭锁是一种同步工具类,可以延迟线程的进度,直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能够通过,当达到结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以确保某些活动直到其他活动都完成之后才继续执行。

  CountDownLatch是一种灵活的闭锁实现,它允许一个或多个线程等待一组事件的产生。闭锁状态包括一个计数器,该计数器初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。

1.1、主要方法

// 构造器,必须指定一个大于零的计数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
} // 线程阻塞,直到计数为0的时候唤醒;可以响应线程中断退出阻塞
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} // 线程阻塞一段时间,如果计数依然不是0,则返回false;否则返回true
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
} // 计数-1
public void countDown() {
sync.releaseShared(1);
} // 获取计数
public long getCount() {
return sync.getCount();
}

    它的内部有一个辅助的内部类:sync

  countDownLath无法被重置,循环栅栏可以。

  循环栅栏是要执行的线程增加,countDownLatch是执行完成的线程增加。

1.2、实现原理

  同ReentrantLock一样,依然是借助AQS的双端队列,来实现原子的计数-1,线程阻塞和唤醒

  参看:013-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架

1. 计数器的初始化

  CountDownLatch内部实现了AQS,并覆盖了tryAcquireShared()tryReleaseShared()两个方法,下面说明干嘛用的

  通过前面的使用,清楚了计数器的构造必须指定计数值,这个直接初始化了 AQS内部的state变量

Sync(int count) {
setState(count);
}

  后续的计数-1/判断是否可用都是基于state进行的

2. countDown() 计数-1的实现

// 计数-1
public void countDown() {
sync.releaseShared(1);
} public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 首先尝试释放锁
doReleaseShared();
return true;
}
return false;
} protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0) //如果计数已经为0,则返回失败
return false;
int nextc = c-1;
// 原子操作实现计数-1
if (compareAndSetState(c, nextc))
return nextc == 0;
}
} // 唤醒被阻塞的线程
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; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // 没有线程被阻塞,直接跳出
break;
}
}

countDown内部实现流程:

  1. 尝试释放锁tryReleaseShared,实现计数-1
  • 若计数已经小于0,则直接返回false
  • 否则执行计数(AQS的state)减一
  • 若减完之后,state==0,表示没有线程占用锁,即释放成功,然后就需要唤醒被阻塞的线程了
  1. 释放并唤醒阻塞线程 doReleaseShared
  • 如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出
  • 头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队

注意:即CountDownLatch计数为0之后,所有被阻塞的线程都会被唤醒,且彼此相对独立,不会出现独占锁阻塞的问题

3. await() 阻塞等待计数为0

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 若线程中端,直接抛异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
} // 计数为0时,表示获取锁成功
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
} // 阻塞,并入队
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) {
// 获取锁成功,设置队列头为node节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) // 线程挂起
&& parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

阻塞的内部实现逻辑

  1. 判断state计数是否为0,不是,则直接放过执行后面的代码
  2. 大于0,则表示需要阻塞等待计数为0
  3. 当前线程封装Node对象,进入阻塞队列
  4. 然后就是循环尝试获取锁,直到成功(即state为0)后出队,继续执行线程后续代码

1.3、使用注意

  • 在创建实例时,必须指定初始的计数值,且应大于0
  • 必须有线程中显示的调用了countDown()计数-1方法;必须有线程显示调用了await()方法(没有这个就没有必要使用CountDownLatch了)
  • 由于await()方法会阻塞到计数为0,如果在代码逻辑中某个线程漏掉了计数-1,导致最终计数一直大于0,直接导致死锁了;
  • 鉴于上面一点,更多的推荐 await(long, TimeUnit)来替代直接使用await()方法,至少不会造成阻塞死只能重启的情况
  • 允许多个线程调用await方法,当计数为0后,所有被阻塞的线程都会被唤醒

1.4、使用场景

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

1.5、示例

  有5个运动员赛跑,为了公平所有运动员需要同时起跑。同时,当最后一个运动员跑完之后,比赛结束。

    public static void main(String[] args) {
//所有线程阻塞,然后统一开始
CountDownLatch begin = new CountDownLatch(1);
//主线程阻塞,直到所有分线程执行完毕
CountDownLatch end = new CountDownLatch(5);
for(int i = 0; i < 5; i++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
begin.await();
System.out.println(LocalDateTime.now()+":"+Thread.currentThread().getName() + " 起跑");
Thread.sleep(new Random().nextInt(2000));
System.out.println(LocalDateTime.now()+":"+Thread.currentThread().getName() + " 到达终点");
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
end.countDown();
} }
}); thread.start();
} try {
System.out.println(LocalDateTime.now()+":"+"1秒后统一开始");
Thread.sleep(1000);
begin.countDown(); end.await();
System.out.println(LocalDateTime.now()+":"+"停止比赛");
} catch (InterruptedException e) {
e.printStackTrace();
} }

结果

2019-02-12T16:51:38.903:1秒后统一开始
2019-02-12T16:51:39.904:Thread-1 起跑
2019-02-12T16:51:39.904:Thread-4 起跑
2019-02-12T16:51:39.904:Thread-3 起跑
2019-02-12T16:51:39.904:Thread-2 起跑
2019-02-12T16:51:39.904:Thread-0 起跑
2019-02-12T16:51:40.035:Thread-0 到达终点
2019-02-12T16:51:40.484:Thread-2 到达终点
2019-02-12T16:51:41.087:Thread-4 到达终点
2019-02-12T16:51:41.215:Thread-3 到达终点
2019-02-12T16:51:41.287:Thread-1 到达终点
2019-02-12T16:51:41.288:停止比赛

示例二、

package com.github.bjlhx15.common.thread.juc.collection;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
// 自定义工作线程
private static class Worker extends Thread {
private CountDownLatch countDownLatch; public Worker(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
} @Override
public void run() {
super.run(); try {
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
// 工作线程开始处理,这里用Thread.sleep()来模拟业务处理
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1); for (int i = 0; i < 3; i++) {
System.out.println("创建工作线程" + i);
Worker worker = new Worker(countDownLatch);
worker.start();
} // 工作线程需要等待主线程准备操作完毕才可以执行,这里用Thread.sleep()来模拟准备操作
Thread.sleep(1000);
System.out.println("主线程准备完毕"); countDownLatch.countDown();
}
}

输出

创建工作线程0
创建工作线程1
创建工作线程2
主线程准备完毕
Thread-0开始执行
Thread-2开始执行
Thread-1开始执行
Thread-2执行完毕
Thread-1执行完毕
Thread-0执行完毕

  自定义的工作线程必须要等主线程准备完毕才可以执行,可以使用CountDownLatch类来帮助我们完成。从程序的执行结果中也可以看出,3个工作线程确实是在主线程准备完毕后才开始执行。

014-并发编程-java.util.concurrent之-CountDownLatch的更多相关文章

  1. 深入理解java:2.3. 并发编程 java.util.concurrent包

    JUC java.util.concurrent包, 这个包是从JDK1.5开始引入的,在此之前,这个包独立存在着,它是由Doug Lea开发的,名字叫backport-util-concurrent ...

  2. Java_并发工具包 java.util.concurrent 用户指南(转)

    译序 本指南根据 Jakob Jenkov 最新博客翻译,请随时关注博客更新:http://tutorials.jenkov.com/java-util-concurrent/index.html.本 ...

  3. Java并发编程-并发工具包(java.util.concurrent)使用指南(全)

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  4. Java 并发工具包 java.util.concurrent 用户指南

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  5. Java 并发工具包 java.util.concurrent 大全

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  6. 013-并发编程-java.util.concurrent.locks之-AbstractQueuedSynchronizer-用于构建锁和同步容器的框架、独占锁与共享锁的获取与释放

    一.概述 AbstractQueuedSynchronizer (简称AQS),位于java.util.concurrent.locks.AbstractQueuedSynchronizer包下, A ...

  7. Java 并发工具包 java.util.concurrent 用户指南(转)

    本文转自http://blog.csdn.net/defonds/article/details/44021605/ 感谢作者 1. java.util.concurrent - Java 并发工具包 ...

  8. 161207、高并发:java.util.concurrent.Semaphore实现字符串池及其常用方法介绍

    实现字符串池: StrPool.java import java.util.ArrayList; import java.util.List; import java.util.concurrent. ...

  9. 018-并发编程-java.util.concurrent.locks之-ReentrantReadWriteLock可重入读写锁

    一.概述 ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程.写线程和写线程同时访问.相对 ...

随机推荐

  1. [Unity3D] 03 - Component of UI

    还需进一步整理! ing... 博客参考 Unity 相关博客:Unity游戏开发爱好者 Unity 3D 连接Mysql数据库 Unity uGUI 登录界面 Unity uGUI 登录及注册功能 ...

  2. 网络编程 -- RPC实现原理 -- NIO多线程 -- 迭代版本V1

    网络编程 -- RPC实现原理 -- 目录 啦啦啦 V1——设置标识变量selectionKey.attach(true);只处理一次(会一直循环遍历selectionKeys,占用CPU资源). ( ...

  3. PDO 基础知识

    PDO: 一.含义: 数据访问抽象层 二.作用 :通过PDO能够访问其它的数据库 三. 用法: 1.造对象 ①$dsn="mysql:dbname=zz(数据库名);host=localho ...

  4. redis数据持久化的两种方式

    1,AOF AOF持久化以日志的形式记录服务器所处理的每一个写.删除操作,查询操作不会记录,以文本的方式append记录,可以打开文件看到详细的操作记录.(相同数量的数据集而言,AOF文件通常要大于R ...

  5. 转发一篇好文:36氪翻译自medium的文章: 读书没有 KPI:为什么坚持“一年读 100 本书”没用?

    你只是为了达成所谓的数量目标而读书. 编者按:读书本是一项安静.缓慢的活动,但随着现代社会节奏的加快,信息技术的广泛普及,读书这一行为模式也开始发生了变化.越来越多的人开始碎片化阅读,并且越来越多的文 ...

  6. Codeforces 1132D - Stressful Training - [二分+贪心+优先队列]

    题目链接:https://codeforces.com/contest/1132/problem/D 题意: 有 $n$ 个学生,他们的电脑有初始电量 $a[1 \sim n]$,他们的电脑每分钟会耗 ...

  7. CCPC-Wannafly Winter Camp Day1 Div1 - 夺宝奇兵 - [贪心+线段树]

    题目链接:https://zhixincode.com/contest/3/problem/J?problem_id=43 样例输入 1 4 1110 11 110 21 210 31 315 415 ...

  8. [No000013B]初识Ildasm.exe——IL反编译的实用工具

    Ildasm.exe 概要: 一.前言: 微软的IL反编译实用程序——Ildasm.exe,可以对可执行文件(ex,经典的控制台Hello World 的 exe 可执行文件)抽取出 IL 代码,并且 ...

  9. Python:导入自定义模块

    在C语言里为了工程文件的主程序main代码简洁的效果,我们经常用include“XXX”的来导入其.h文件 在Python里Import自己的自定义模块需要注意几个坑 以main主函数和需要导入的ha ...

  10. MySQL命令:约束

    约束是一种限制,它通过对表的行或列的数据做出限制,来确保表的数据的完整性.唯一性 约束分类: 约束类型与关键字: 主键      PRIMARY KEY 默认值  DEFAULT 唯一      UN ...