转:https://blog.csdn.net/hbtj_1216/article/details/109655995

1 概念
1.1 简介

  CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行

1.2 用法
  典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

  典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒

2 方法
  CountDownLatch 提供了一些方法:
    1)await()    使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
    2)await(long timeout, TimeUnit unit)    带超时时间的await()。
    3)countDown()    使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
    4)getCount()    获得latch的数值。

3 例子
  下面代码演示2个等待线程通过CountDownLatch去等待3个工作线程完成操作:

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
// 让2个线程去等待3个三个工作线程执行完成
CountDownLatch c = new CountDownLatch(3); // 2 个等待线程
WaitThread waitThread1 = new WaitThread("wait-thread-1", c);
WaitThread waitThread2 = new WaitThread("wait-thread-2", c); // 3个工作线程
Worker worker1 = new Worker("worker-thread-1", c);
Worker worker2 = new Worker("worker-thread-2", c);
Worker worker3 = new Worker("worker-thread-3", c); // 启动所有线程
waitThread1.start();
waitThread2.start();
Thread.sleep(1000);
worker1.start();
worker2.start();
worker3.start();
}
}

等待线程

class WaitThread extends Thread {

    private String name;
private CountDownLatch c; public WaitThread(String name, CountDownLatch c) {
this.name = name;
this.c = c;
} @Override
public void run() {
try {
// 等待
System.out.println(this.name + " wait...");
c.await();
System.out.println(this.name + " continue running...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

 工作线程

class Worker extends Thread {

    private String name;
private CountDownLatch c; public Worker(String name, CountDownLatch c) {
this.name = name;
this.c = c;
} @Override
public void run() {
System.out.println(this.name + " is running...");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + " is end.");
c.countDown();
}
}

运行结果

wait-thread-1 wait...
wait-thread-2 wait...
worker-thread-3 is running...
worker-thread-2 is running...
worker-thread-1 is running...
worker-thread-1 is end.
worker-thread-3 is end.
worker-thread-2 is end.
wait-thread-1 continue running...
wait-thread-2 continue running... Process finished with exit code 0

4 源码解析

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) {
// Decrement count; signal when transition to zero
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() + "]";
}
}

4.1 Sync 内部类
  CountDownLatch通过内部类Sync来实现同步语义。
  Sync继承AQS,源码如下:

    private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L; // 设置同步状态的值
Sync(int count) {
setState(count);
} // 获取同步状态的值
int getCount() {
return getState();
} // 尝试获取同步状态,只有同步状态的值为0的时候才成功
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
} // 尝试释放同步状态,每次释放通过CAS将同步状态的值减1
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
// 如果同步状态的值已经是0了,不要再释放同步状态了,也不要减1了
if (c == 0)
return false;
// 减1
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}

4.2 await() 源码解析

 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);
}

首先,通过tryAcquireShared(arg)尝试获取同步状态,具体的实现被Sync重写了,查看源码:

protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

如果同步状态的值为0,获取成功。这就是CountDownLatch的机制,尝试获取latch的线程只有当latch的值减到0的时候,才能获取成功。
如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:

 private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程加入同步队列的尾部
final Node node = addWaiter(Node.SHARED);
try {
// 自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头结点,则尝试获取同步状态
if (p == head) {
// 当前节点尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果获取成功,则设置当前节点为头结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 如果当前节点的前驱不是头结点,尝试挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}

这里,调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 挂起当前线程。

4.3 countDown() 源码解析

public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 尝试释放同步状态
if (tryReleaseShared(arg)) {
// 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点
doReleaseShared();
return true;
}
return false;
}

首先,通过tryReleaseShared(arg)尝试释放同步状态,具体的实现被Sync重写了,源码:

protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
// 同步状态值减1
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

如果同步状态值减到0,则释放成功,进入自旋,尝试唤醒同步队列中头结点的后继节点,调用的是AQS的doReleaseShared()函数:

 private void doReleaseShared() {
for (;;) {
// 获取头结点
Node h = head;
if (h != null && h != tail) {
// 获取头结点的状态
int ws = h.waitStatus;
// 如果是SIGNAL,尝试唤醒后继节点
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒头结点的后继节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

这里调用了unparkSuccessor(h)去唤醒头结点的后继节点。

4.4 如何唤醒所有调用 await() 等待的线程
  此时这个后继节点被唤醒,那么又是如何实现唤醒所有调用await()等待的线程呢?
  回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)方法中:

 private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程加入同步队列的尾部
final Node node = addWaiter(Node.SHARED);
try {
// 自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头结点,则尝试获取同步状态
if (p == head) {
// 当前节点尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {
// 如果获取成功,则设置当前节点为头结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 如果当前节点的前驱不是头结点,尝试挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}

该方法里面,通过调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()将线程挂起。
当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。
此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。
查看源码:

    // 如果执行这个函数,那么propagate一定等于1
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;
// 如果下一个节点为null或者节点为shared节点
if (s == null || s.isShared())
doReleaseShared();
}
}

这里,当前节点被唤醒,首先设置当前节点为头结点。
如果当前节点的下一个节点是shared节点,调用doReleaseShared(),源码:

 private void doReleaseShared() {
// 自旋
for (;;) {
// 获取头结点,也就是当前节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果head没有改变,则调用break退出循环
if (h == head)
break;
}
}

  首先,注意if (h == head) break; 这里每次循环的时候判断head头结点有没有改变,如果没有改变则退出循环。因为只有当新的节点被唤醒之后,新节点才会调用setHead(node)设置自己为头结点,头结点才会改变。
  其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)。
  如果队列中有两个或以上个节点,那么检查局部变量h的状态:
       如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒。
       如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
       如果状态为PROPAGATE,直接判断head是否变化。

线程基础知识-CountDownLatch的更多相关文章

  1. Java__线程---基础知识全面实战---坦克大战系列为例

    今天想将自己去年自己编写的坦克大战的代码与大家分享一下,主要面向学习过java但对java运用并不是很熟悉的同学,该编程代码基本上涉及了java基础知识的各个方面,大家可以通过练习该程序对自己的jav ...

  2. java线程基础知识----线程与锁

    我们上一章已经谈到java线程的基础知识,我们学习了Thread的基础知识,今天我们开始学习java线程和锁. 1. 首先我们应该了解一下Object类的一些性质以其方法,首先我们知道Object类的 ...

  3. java线程基础知识----线程基础知识

    不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...

  4. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  5. Java线程基础知识(状态、共享与协作)

    1.基础概念 CPU核心数和线程数的关系 核心数:线程数=1:1 ;使用了超线程技术后---> 1:2 CPU时间片轮转机制 又称RR调度,会导致上下文切换 什么是进程和线程 进程:程序运行资源 ...

  6. Java并发之线程管理(线程基础知识)

    因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录.当然,学习书中知识的时候自己的思考和实践是最重要的.说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的.所以想着细 ...

  7. java线程基础知识----java daemon线程

    java线程是一个运用很广泛的重点知识,我们很有必要了解java的daemon线程. 1.首先我们必须清楚的认识到java的线程分为两类: 用户线程和daemon线程 A. 用户线程: 用户线程可以简 ...

  8. java并发编程(一)----线程基础知识

    在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线 ...

  9. Java 线程基础知识

    前言 什么是线程?线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程 ID,当前指令指针 (PC),寄存器集合和堆栈组成.另外,线 ...

  10. Delphi线程基础知识

    参考http://blog.chinaunix.net/uid-10535208-id-2949323.html 一.概述 Delphi提供了好几种对象以方便进行多线程编程.多线程应用程序有以下几方面 ...

随机推荐

  1. Perl读取Excel中的数据

    #!usr/bin/perl -W use strict; use Spreadsheet::ParseExcel;#PERL的Spreadsheet::ParseExcel模块支持Excel的读操作 ...

  2. 第一章:使用函数绘制matplotlib的图表组成元素

    1.绘制直线图 1 # ============================展现变量的趋势变化========================== 2 import matplotlib.pypl ...

  3. 【笔面试真题】Flow++赋乐科技-面试-2022年1月25日

    一.概括 涉及JVM的GC.三色标记 并发部分的锁 Java集合中的hashmap.list kafka中ISR相关 硬件相关-有无DMA 自定义类(代码) 缺陷:锁.list 二.JVM相关内容 1 ...

  4. JavaFX入门笔记

    JavaFX入门笔记 背景 Java选修课第四次实验 所需工具 IDEA JavaFX插件(需要Maven) JavaFX Scene Builder 参考资料 https://www.yiibai. ...

  5. Redis的数据被删除,占用内存咋还那么大?

    通过 CONFIG SET maxmemory 100mb 或者在 redis.conf 配置文件设置 maxmemory 100mb Redis 内存占用限制.当达到内存最大值值,会触发内存淘汰策略 ...

  6. python什么是异常?如何处理异常

    异常处理 什么是异常 异常是程序错误发生的信号.程序一旦出现错误,就会产生一个异常,如果程序中没有处理该异常,该异常就会抛出来,程序的运行也随即终止. 错误分为两种 1.语法错误 2.逻辑错误 如何处 ...

  7. STM32用PWM波控制呼吸灯代码

    pwm.c #include "pwm.h" //TIM3-CH3 //PB0 void PWM_Config(void) { GPIO_InitTypeDef GPIO_Init ...

  8. JavaScript:操作符:算术运算符(加减乘除模幂)及其隐式转换数据类型

    加法+ 减法- 乘法* 除法/ 模运算% 幂运算**,即a ** b求的是a的b次方 执行上述运算时,当两个操作数有非数字时,JS会隐式转换为数字,再进行运算: 一些特殊的非数字,会进行如下转换: t ...

  9. Netty-BIO、NIO、AIO、零拷贝-2

    Java BIO 编程 一.I/O 模型 1.I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能 2.Java 共支持 3 种网络编程模型/IO 模式:BI ...

  10. Service层和Dao层的一些自我理解(╥╯^╰╥)(╥╯^╰╥)(学了这么久,这玩意儿似懂非懂的)

    学习java已经有很长时间了,但由于是在学校学的,基础不怎么扎实. 这几个月系统的学习,弥补了很多的缺陷,虽然大多数时间都在弄算法(咳咳),我前面的博客有写 如果有认真看过我代码的朋友会发现,我其实英 ...