CountDownLatch是java并发包中辅助并发的工具类,目的是让并发运行的代码在某一个执行点阻塞,直到所有条件都满足,这里的条件就是调用countDown()方法,有点类似计数器的功能。

用法如

    public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
//如果没有countDown()操作,直接调用await方法则永远不会打印最后一行
countDownLatch.countDown();
countDownLatch.countDown();
countDownLatch.await();
System.out.println("运行结束");
}

构造函数中传入的数字2,表示需要2次countDown()方法调用,否则代码会一直阻塞在await()方法调用处  

比较常见的用法如,主线程声明一个CountDownLatch,然后多线程countDown,主线程在等待

     public static void testMultiThread() throws InterruptedException {
int threadNum = 2;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
for(int i=0;i<threadNum;i++){
executorService.execute(new Runnable() {
public void run() {
System.out.println("我的任务,打印出一句话");
countDownLatch.countDown();
}
});
}
countDownLatch.await();
System.out.println("全部任务都结束了,欧耶");
executorService.shutdown();
}

运行结果

我的任务,打印出一句话
我的任务,打印出一句话
全部任务都结束了,欧耶 Process finished with exit code 0

如果把第三行代码修改成

 final CountDownLatch countDownLatch = new CountDownLatch(threadNum+1);

  

那么程序将永远无法打印出
全部任务都结束了,欧耶

以上是这个类的表象行为,那么它是如何在多线程做到这样的功能呢

先来看看它的类结构

CountDownLacth中,有6个public的方法,一个内部私有类Sync,及一个Sync实例的变量sync

Sync是这个类的关键,它保证了countDown(),await()方法在多线程场景下可以保证countDownLatch的可见性(正常的同步)

我们先来自己实现一个CountDownLacth类,使用synchronized关键字实现

MyCountDownLatch模拟了countDown和await方法,通过synchronized和私有变量state来达到这个目的。synchronized的劣势在于锁机制完全互斥,并发量高时性能下降比较明显,无法维持常态化的性能(JDK 5)。因此CountDownLatch以及并发包中的类都采用了取巧的方式,通过线程自旋来追求线程响应时间,而不是让线程只能一直等待锁被释放再竞争。1.6之后,synchronized的性能和ReentrantLock的性能其实已经相当,偏向锁也改进了一个线程重复获取锁时不需要cpu切换上下文。

private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;

     //初始化总的countdown次数值,这个操作涉及内存语义 volatile写
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;
}
}
}

  

CountDownLatchd countDown方法会调用

Sync的tryReleaseShared去将计数器-1
public void countDown() {
sync.releaseShared(1);
}

 

AbstractQueuedSynchronizer类方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

 

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} AbstractQueuedSynchronizer类方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
} AbstractQueuedSynchronizer类方法
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
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) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  

 

 

await()方法Sync的tryAcquireShared方法不断判断计数器值为0,方法返回正数,await方法直接返回,亦即是获取了锁
 

  

AbstractQueuedSynchronizer类是concurrent包的一个基础类,基于它,我们可以实现并发安全的诸多功能,例如CountDownLacth
    /**
* The synchronization state.
*/
private volatile int state;

  

AbstractQueuedSynchronizer类有一个volatile的变量,CountDownLacth中的Sync类,getState()方法其实就是获取它的值
由于volatile关键字的特殊内存语义,当它被修改时,将本地高速缓存中的值写到主存中去,当它的值被读取时,会把本地缓存中state置为无效,到主存获取值,因此每个线程都能获取到他最新的值
 
   countDownLatch.countDown();
-> sync.releaseShared(1);
-> public final boolean releaseShared(int arg) {
//countdown时,尝试判断当前状态,如果状态已经可以完全释放锁时,进行释放锁操作
if (tryReleaseShared(arg)) {
doReleaseShared(); //释放共享锁
return true;
}
return false;
} /**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/* 判断是否已经有线程(node)在等待,如果在等待,则从head开始,寻找后继节点,并唤醒他们
* 直到最后head 为null或head == tail,才退出,退出时,已然唤醒了所有需要唤醒的线程
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head; //等待获取锁的队列
if (h != null && h != tail) { //head == tail 时,是一个特殊情况,是第一个node入队时的临时状态,此时head节点上没有等待的线程
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); //head唤醒后继线程
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //尝试设置waitStatus状态为PROPAGATE
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

  

	countDownLatch.await();
-> sync.acquireSharedInterruptibly(1);
->
/**
* Acquires in shared mode, aborting if interrupted. Implemented
* by first checking interrupt status, then invoking at least once
* {@link #tryAcquireShared}, returning on success. Otherwise the
* thread is queued, possibly repeatedly blocking and unblocking,
* invoking {@link #tryAcquireShared} until success or the thread
* is interrupted.
* @param arg the acquire argument.
* This value is conveyed to {@link #tryAcquireShared} but is
* otherwise uninterpreted and can represent anything
* you like.
* @throws InterruptedException if the current thread is interrupted
*/
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //尝试判断共享锁状态,即判断state是否为0了,如果已经为0,表示期待的状态已经达到了,锁的状态已经标识不需等待了,直接返回
doAcquireSharedInterruptibly(arg); //为0后
} /**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED); //当前线程装入node,此时等待队列不为空了
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); //当前线程的前继线程
if (p == head) { //前继为head
int r = tryAcquireShared(arg); //判断state状态,是否已经ok
if (r >= 0) { //state值为0时,r == 1
setHeadAndPropagate(node, r); //当前node设置为head,尝试获取下一个node
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //尝试park(阻塞)当前线程,有点像进入wait状态,处理器可能不会分配时间
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

 这里需要说明一下,node的waitStatus是在何时被设置为Node.SIGNAL状态的

shouldParkAfterFailedAcquire方法中,pred参数为当前node的前继node,方法一进来就判断了pred.waitStatus是否为Node.SIGNAL,如果已经是这个状态了就直接返回
ws>0表示CANCELLED状态,因为其他状态都是<=0的,此时遍历的将这种无效的node去掉;如果ws <= 0,就将pred的状态变成Node.SIGNAL,并返回false;这里可以看到,如果前缀node已经是SIGNAL状态
就会park当前节点线程,如果,前缀node是未cancel状态,就设置为SIGNAL状态。这样,在调用await时,如果无法立即返回,就会将当前线程阻塞,并设置前置node状态为SIGNAL。这也对应了releaseShared
中的Node.SIGAL状态的判断。await()是从tail -> head方向做的;countdown(releaseShared)的方向是从head -> tail的,这样,等待锁的线程不断判断前缀node,释放锁的线程不断更新head -> tail
状态,随着head -> tail状态更新完毕,await等待锁也等到了p == head,返回true
  /**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

  

Java的LockSupport.park()实现分析 参见博客: https://blog.csdn.net/hengyunabc/article/details/28126139

附waitStatus的各种状态

	/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block. 线程的后继线程正/已被阻塞,当该线程release或cancel时,要唤醒这个后继线程(unpark)
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks. 由于timeout或线程被中断时的状态
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.) 表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened. 传播共享锁,只能在head上设置这个状态
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;

  


CountDownLatch的实现原理的更多相关文章

  1. Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析

    1.简介 在分析完AbstractQueuedSynchronizer(以下简称 AQS)和ReentrantLock的原理后,本文将分析 java.util.concurrent 包下的两个线程同步 ...

  2. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

  3. 并发——深入分析CountDownLatch的实现原理

    一.前言   最近在研究java.util.concurrent包下的一些的常用类,之前写了AQS.ReentrantLock.ArrayBlockingQueue以及LinkedBlockingQu ...

  4. Java并发包中CountDownLatch的工作原理、使用示例

    1. CountDownLatch的介绍 CountDownLatch是一个同步工具,它主要用线程执行之间的协作.CountDownLatch 的作用和 Thread.join() 方法类似,让一些线 ...

  5. Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例

    概要 前面对"独占锁"和"共享锁"有了个大致的了解:本章,我们对CountDownLatch进行学习.和ReadWriteLock.ReadLock一样,Cou ...

  6. Java并发包5--同步工具CountDownLatch、CyclicBarrier、Semaphore的实现原理解析

    前言: JUC中提供了很多同步工具类,比如CountDownLatch.CyclicBarrier.Semaphore等,都可以作用同步手段来实现多线程之间的同步效果 一.CountDownLatch ...

  7. CountDownLatch原理详解

    介绍 当你看到这篇文章的时候需要先了解AQS的原理,因为本文不涉及到AQS内部原理的讲解. CountDownLatch是一种同步辅助,让我们多个线程执行任务时,需要等待线程执行完成后,才能执行下面的 ...

  8. Java并发系列[7]----CountDownLatch源码分析

    CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须 ...

  9. CountDownLatch 使用说明

    CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成. CountDownLatch的用法非常简单,下 ...

随机推荐

  1. WinForm 菜单控件

    一:MenuStrip 菜单条 MenuStrip 是应用程序菜单条的容器. 二:ToolStripMenuItem 像上面图中, 文件 格式 等这些菜单当中的一级菜单以及文件中的 新建 打开 分割条 ...

  2. C++ stl 运用(深层)

    1.multiset(set差不多) (1)erase删除,删除指针和键值是不同的. 键值的话是删除所有,指针的话是那个位置的值. (2)统计单个键值个数. (3)对于q.begin(),q.end( ...

  3. JavaScript sort() 方法详解

    定义和用法 sort() 方法用于对数组的元素进行排序. 语法 arrayObject.sort(sortby) 参数 描述 sortby 可选.规定排序顺序.必须是函数. 返回值 对数组的引用.请注 ...

  4. Golang源码探索(二) 协程的实现原理

    Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻 ...

  5. CentOS系统中出现错误--SSH:connect to host centos-py port 22: Connection refused

    我在第一次搭建自己的 hadoop2.2.0单节点的伪分布集成环境时遇到了此错误,通过思考问题和查找解决方案最终搞定了这个问题,其错误原因主要有以下几种: 1)SSH服务为安装 此时,采用在线安装的方 ...

  6. spring+struts2+hibernate整合

    web.xml需要配置 <context-param> <param-name>contextConfigLocation</param-name> <par ...

  7. HTML出现错位的问题

    引起网页HTML显示错位的几个常见问题: 1.在HTML代码中缺失元素的开始或结束标签 2.CSS设置中对边界.填充或边框的设置超出了父级容器的范围 3.CSS和HTML的编码不统一 4.浏览器的解析 ...

  8. Windows常用shell命令大全

    Windows常用shell命令大全 基于鼠标操作的后果就是OS界面外观发生改变, 就得多花学习成本.更主要的是基于界面引导Path与命令行直达速度是难以比拟的.另外Geek很大一部分是键盘控,而非鼠 ...

  9. 逆向实战第一讲,寻找OllyDbg调试工具的Bug并修复

    逆向实战第一讲,寻找OllyDbg调试工具的Bug并修复 首先我们要知道这个OD的Bug是什么. 我们调试一个UNICODE的窗口,看下其窗口过程. 一丶查看OllyDbg 的Bug 1.1spy++ ...

  10. 原码、反码、补码的正(nao)确(can)打开方式

    我们知道日常生活中使用的数分为整数和实数,整数的小数点固定在数的最右边,可以省略不写,而实数的小数点则不固定.在计算机中只能识别和表示“0”和“1”,而无法识别小数点,因此要想使得计算机能够处理日常使 ...