CountDownLatch的实现原理
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的实现原理的更多相关文章
- Java 线程同步组件 CountDownLatch 与 CyclicBarrier 原理分析
1.简介 在分析完AbstractQueuedSynchronizer(以下简称 AQS)和ReentrantLock的原理后,本文将分析 java.util.concurrent 包下的两个线程同步 ...
- 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理
1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...
- 并发——深入分析CountDownLatch的实现原理
一.前言 最近在研究java.util.concurrent包下的一些的常用类,之前写了AQS.ReentrantLock.ArrayBlockingQueue以及LinkedBlockingQu ...
- Java并发包中CountDownLatch的工作原理、使用示例
1. CountDownLatch的介绍 CountDownLatch是一个同步工具,它主要用线程执行之间的协作.CountDownLatch 的作用和 Thread.join() 方法类似,让一些线 ...
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
概要 前面对"独占锁"和"共享锁"有了个大致的了解:本章,我们对CountDownLatch进行学习.和ReadWriteLock.ReadLock一样,Cou ...
- Java并发包5--同步工具CountDownLatch、CyclicBarrier、Semaphore的实现原理解析
前言: JUC中提供了很多同步工具类,比如CountDownLatch.CyclicBarrier.Semaphore等,都可以作用同步手段来实现多线程之间的同步效果 一.CountDownLatch ...
- CountDownLatch原理详解
介绍 当你看到这篇文章的时候需要先了解AQS的原理,因为本文不涉及到AQS内部原理的讲解. CountDownLatch是一种同步辅助,让我们多个线程执行任务时,需要等待线程执行完成后,才能执行下面的 ...
- Java并发系列[7]----CountDownLatch源码分析
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须 ...
- CountDownLatch 使用说明
CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成. CountDownLatch的用法非常简单,下 ...
随机推荐
- App 组件化/模块化之路——使用SDK的思路进行模块化设计接口
在不久之前分享一篇<App 组件化/模块化之路——如何封装网络请求框架>文章介绍了我在项目中封装网络请求框架的思路.开发一个 App 会涉及到很多网络请求 API ,例如登录注册接口.用户 ...
- 机器学习之三:logistic回归(最优化)
一般来说,回归不用在分类问题上,因为回归是连续型模型,而且受噪声影响比较大.如果非要应用进入,可以使用logistic回归. logistic回归本质上是线性回归,只是在特征到结果的映射中加入了一层函 ...
- runtime--小白看过来
目录 RunTime 概述 RunTime消息机制 RunTime交换方法 RunTime消息转发 RunTime关联对象 RunTime实现字典与模型互转 1.RunTime 概述 我们在面试的时候 ...
- Buy the Ticket(卡特兰数+递推高精度)
Buy the Ticket Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Tota ...
- Scala从入门到精通之四-映射和元组
在Scala中映射之键值对的集合,元组是n个对象的聚集,但是对象的类型不一定相同 本节内容要点 Scala中映射的创建,遍历和查询 如何从可变和不可变映射中做出选择 Scala映射和Java映射见的互 ...
- js 立即调用的函数表达式
当你声明类似function foo(){}或var foo = function(){}函数的时候,通过在后面加个括弧就可以实现自执行,例如foo(),看代码: // 因为想下面第一个声明的func ...
- MSBuild Tools解决办法
每次在CI上通过Msbuild做发布,基本都会碰到下面的问题 error MSB4019: 未找到导入的项目"C:\Program Files (x86)\MSBuild\Microsoft ...
- HTML知识速递
1.html的定义 超文本标记语言(Hypertext Markup Language,HTML)通过标签语言来标记要显示的网页中的各个部分.一套规则,浏览器认识的规则 浏览器按顺序渲染网页文件,然后 ...
- 使用java生成mapbox-gl可读的vector tile
概述 mapbox-gl主要数据源来自mapbox vector tile,本文就是要阐述怎样把postgresql中的地理空间数据转换成vector tile,流程图如下: 配置 该工程采用spri ...
- centos7安装python3和Django后,ModuleNotFoundError: No module named '_sqlite3'
1.准备安装环境 yum groupinstall 'Development Tools' yum install zlib-devel bzip2-devel openssl-devel ncurs ...