深入浅出Java并发包—CountDownLauch原理分析 (转载)
转载地址:http://yhjhappy234.blog.163.com/blog/static/3163283220135875759265/
CountDownLauch是Java并发包中的一个同步工具集,常被人们称之为并发中的计数器,还有一种被成为闭锁!
CountDownLauch主要使用在两种场景,一种被称为开关,它允许一个任务完成之前,一个或一组线程持续等待。此种情况经常被称之为闭锁,通俗的讲就是,相当于一扇大门,在大门打开之前所有线程都被阻断,一旦大门打开,所有线程都将通过,但是一旦大门打开,所有线程都通过了,那么这个闭锁的状态就失效了,门的状态也就不能变了,只能是打开状态。另一种场景经常被称之为计数器,它允许将一个任务拆分为N个小任务,主线程在所有任务完成之前一直等待,每个任务完成时将计数器减一,直到所有任务完成后取消主线程的阻塞。
我们来看一下对应CountDownLauch对应的API。
构造方法摘要 | |
---|---|
CountDownLatch(int count) 构造一个用给定计数初始化的 DE>CountDownLatchDE>。 |
方法摘要 | |
---|---|
void |
await() 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。 |
boolean | await(long timeout, TimeUnit unit) 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。 |
void | countDown() 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。 |
long | getCount() 返回当前计数。 |
String | toString() 返回标识此锁存器及其状态的字符串。 |
CountDownLatch维护了一个正数计数器,countDown方法对计数器做减操作,await方法等待计数器达到0。所有await的线程都会阻塞直到计数器为0或者等待线程中断或者超时。
我们分别来看一下对应的一个应用实例:
package com.yhj.lauth; import java.util.Date; import java.util.concurrent.CountDownLatch; //工人 class Worker extends Thread{ private int workNo;//工号 private CountDownLatch startLauch;//启动器-闭锁 private CountDownLatch workLauch;//工作进程-计数器 public Worker(int workNo,CountDownLatch startLauch,CountDownLatch workLauch) { this.workNo = workNo; this.startLauch = startLauch; this.workLauch = workLauch; } @Override public void run() { try { System.out.println(new Date()+" - YHJ"+workNo+" 准备就绪!准备开工!"); startLauch.await();//等待老板发指令 System.out.println(new Date()+" - YHJ"+workNo+" 正在干活..."); Thread.sleep(100);//每人花100ms干活 } catch (InterruptedException e) { e.printStackTrace(); }finally{ System.out.println(new Date()+" - YHJ"+workNo+" 工作完成!"); workLauch.countDown(); } } } //测试用例 public class CountDownLauthTestCase { public static void main(String[] args) throwsInterruptedException { int workerCount = 10;//工人数目 CountDownLatch startLauch = new CountDownLatch(1);//闭锁 相当于开关 CountDownLatch workLauch = newCountDownLatch(workerCount);//计数器 System.out.println(new Date()+" - Boss:集合准备开工了!"); for(int i=0;i<workerCount;++i){ new Worker(i, startLauch, workLauch).start(); } System.out.println(new Date()+" - Boss:休息2s后开工!"); Thread.sleep(2000); System.out.println(new Date()+" - Boss:开工!"); startLauch.countDown();//打开开关 workLauch.await();//任务完成后通知Boss System.out.println(new Date()+" - Boss:不错!任务都完成了!收工回家!"); } } 执行结果: Sat Jun 08 18:59:33 CST 2013 - Boss:集合准备开工了! Sat Jun 08 18:59:33 CST 2013 - YHJ0 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ2 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ1 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ4 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - Boss:休息2s后开工! Sat Jun 08 18:59:33 CST 2013 - YHJ8 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ6 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ3 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ7 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ5 准备就绪!准备开工! Sat Jun 08 18:59:33 CST 2013 - YHJ9 准备就绪!准备开工! Sat Jun 08 18:59:35 CST 2013 - Boss:开工! Sat Jun 08 18:59:35 CST 2013 - YHJ0 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ2 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ1 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ4 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ8 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ6 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ3 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ7 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ5 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ9 正在干活... Sat Jun 08 18:59:35 CST 2013 - YHJ5 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ1 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ3 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ6 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ7 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ9 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ4 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ0 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ2 工作完成! Sat Jun 08 18:59:35 CST 2013 - YHJ8 工作完成! Sat Jun 08 18:59:35 CST 2013 - Boss:不错!任务都完成了!收工回家! |
这个示例里面使用了两个CountDownLauch,分别构建了两种场景,第一个startLauch相当于开关,在开启之前,没有任何一个线程执行,当开启之后,所有线程同时可以执行。第二个workerLauch其实就是一个计数器,当计数器没有减到零的时候,主线程一直等待,当所有线程执行完毕后,主线程取消阻塞继续执行!
第二种场景在我们后面要学习的线程池中经常会用到,我们后续再讨论!
此处还有一个重要的特性,就是
内存一致性效果:线程中调用 countDown() 之前的操作 happen-before 紧跟在从另一个线程中对应 await() 成功返回的操作。
场景应用我们是看到了,那它到底是基于什么原理,怎么实现的呢?
我们来看下对应的源码:
private static final class Sync extends AbstractQueuedSynchronizer |
类的第二行我们就看到了其内部实现了AQS的一个同步器。我们重点来看下我们用到的几个方法:await和countDown。首先来看await方法
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } |
很明显是直接调用内部重新实现的同步器的获取共享锁的方法(前面我们一直再讲独占锁,今天我们借此机会把共享锁的机制一起讲掉)。
public final void acquireSharedInterruptibly(int arg) throwsInterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } |
此处如果线程中断,则直接退出,否则尝试获取共享锁,我们来看下tryAcquireShared(arg)的实现(此方法由内部类重写实现):
public int tryAcquireShared(int acquires) { return getState() == 0? 1 : -1; } |
所谓共享锁是说所有共享锁的线程共享同一个资源,一旦任意一个线程拿到共享资源,那么所有线程就都拥有的同一份资源。也就是通常情况下共享锁只是一个标志,所有线程都等待这个标识是否满足,一旦满足所有线程都被激活(相当于所有线程都拿到锁一样)。这里的闭锁CountDownLatch就是基于共享锁的实现。和明显这里的标识就是state等不等于零,而state其实是有多少个线程在竞争这份资源,我们前面可以看到是通过构造函数传入的一个大于0的数据,因此此时此刻此处返回的永远是-1。
Sync(int count) { setState(count); } |
当tryAcquireShared返回的数据小于零,说明没有获取到资源,需要阻塞,此时执行代码doAcquireSharedInterruptibly():
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()) break; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } // Arrive here only if interrupted cancelAcquire(node); throw new InterruptedException(); } |
这里首先以共享模式添加一个节点加入到CLH队列中去,然后检查当前节点的前继节点(插入的数据在队尾),如果前继节点是头结点并且当前的计数器为0的话,则唤醒后继节点(唤醒后面来讲),否则判断是否需要阻塞,如果需要,则阻塞当前线程!直到被唤醒或被中断!
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } |
这里注意一点,LockSupport.park(Obj)中的参数obj是阻塞的监视对象,而非阻塞的对象,阻塞的对象是当前操作的线程,所以unpack的时候也是应该结算对应的线程!不要搞混了哈!
public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); unsafe.park(false, 0L); setBlocker(t, null); } public static void unpark(Thread thread) { if (thread != null) unsafe.unpark(thread); } |
下面我们来看一下对应的countDown方法的实现
public void countDown() { sync.releaseShared(1); } |
首先每执行一次countDown就会执行内部方法的一次释放锁的操作!
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } |
如果尝试成功则设置当前节点为头结点,并唤醒对应节点的后继节点!
public 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; } } |
同样,释放锁的方法也是CountDownLauch内部的同步类自己实现,这个方法自旋检测当前计数器的数目,如果等于零,说明之前阻塞的线程已经全部释放了,直接返回false,否则CAS设置当前的计数器,减去countdown的数目,如果设置成功后的数据为零的话,说明已经全部执行完毕,需要释放阻塞的线程了,返回true(注意此处精妙的返回nextc == 0),否则返回false。
我们再来回看releaseShared方法,当tryReleaseShared返回true的时候,说明计数器已经为零,阻塞的资源需要释放了!此时执行unparkSuccessor(h)方法唤醒队列中的头结点。
此处设计了一个精妙的队列依次去释放被阻塞的线程,而不是类似singleAll的方法直接唤醒所有线程。那到底它是怎么实现的呢?我们代码上看只唤醒了头结点(其实是头结点的后继节点,头结点只是一个空节点),我们先来看下unparkSuccessor的实现
private void unparkSuccessor(Node node) { /* * Try to clear status in anticipation of signalling. It is * OK if this fails or if status is changed by waiting thread. */ compareAndSetWaitStatus(node, Node.SIGNAL, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); } |
明显我们可以看到,传入的参数为头结点,通过CAS设置数据后,唤醒了头结点的后继结点(注意unpack的是线程而不是阻塞监视器)。然后就返回了!
那剩余阻塞的线程是怎么唤醒的呢?我们再来看下await方法中doAcquireSharedInterruptibly的实现
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); // tag 2 if (r >= 0) { setHeadAndPropagate(node, r); // tag 3 p.next = null; // help GC return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())// tag 1 break; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } // Arrive here only if interrupted cancelAcquire(node); throw new InterruptedException(); } |
前面我们可以看到在执行parkAndCheckInterrupt()时进行了阻塞,当我们唤醒头结点的后继节点(第一个进入队列的节点)时,tag1此行代码被唤醒,break之后继续进入自旋,而此时tag2行代码检测到计数器已经为0,因此tryAcquireShared(arg)返回的结果是1(之前返回的都是-1),r大于零,进入tag3代码,tag3会把当前的线程设置为头结点,然后继续唤醒后续的后继节点。
private void setHeadAndPropagate(Node node, int propagate) { setHead(node); // tag 4 if (propagate > 0 && node.waitStatus != 0) { /* * Don't bother fully figuring out successor. If it * looks null, call unparkSuccessor anyway to be safe. */ Node s = node.next; if (s == null || s.isShared()) unparkSuccessor(node); // tag 5 } } |
后继节点被唤醒后,则继续唤醒后面的后继节点,进而把队列中的数据依次唤醒!
整个CountDownLatch就是这个样子的。其实有了前面原子操作和AQS的原理及实现,分析CountDownLatch还是比较容易的。
深入浅出Java并发包—CountDownLauch原理分析 (转载)的更多相关文章
- 原子类java.util.concurrent.atomic.*原理分析
原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...
- JAVA常用数据结构及原理分析
JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ...
- 深入浅出Java并发包—锁机制(三)
接上文<深入浅出Java并发包—锁机制(二)> 由锁衍生的下一个对象是条件变量,这个对象的存在很大程度上是为了解决Object.wait/notify/notifyAll难以使用的问题. ...
- 深入浅出Java并发包—锁机制(二)
接上文<深入浅出Java并发包—锁机制(一) > 2.Sync.FairSync.TryAcquire(公平锁) 我们直接来看代码 protected final boolean tr ...
- Java NIO使用及原理分析 (四)
在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...
- (6)Java数据结构-- 转:JAVA常用数据结构及原理分析
JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ...
- Java NIO使用及原理分析 (四)(转)
在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...
- (转载)Java NIO:NIO原理分析(二)
NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如果我们使用 ...
- Java NIO使用及原理分析(1-4)(转)
转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...
随机推荐
- k8s的两种网络方案与多种工作模式[flannel与calico]
k8s的两种网络方案与多种工作模式 1. Flannel: flannel有三种工作模式: 1. vxlan(隧道方案) 2. host-gw(路由方案) 2. udp(在用户态实现的数据封装解封装, ...
- 笨办法学python - 专业程序员的养成完整版PDF免费下载_百度云盘
笨办法学python - 专业程序员的养成完整版PDF免费下载_百度云盘 提取码:xaln 怎样阅读本书 由于本书结构独特,你必须在学习时遵守几条规则 录入所有代码,禁止复制粘贴 一字不差地录入代码 ...
- chromedp入门
chromedp入门 chromedp是什么? chromedp是go写的,支持Chrome DevTools Protocol 的一个驱动浏览器的库.并且它不需要依赖其他的外界服务(比如 Selen ...
- 搜索引擎-SHODAN
shodan这个搜索引擎不会爬取网页内容,而是爬取所有的联网设备. 这个搜索引擎还是很强大的,下图就是我用shodan查自己的案例服务器的结果: 如图,可以查到这台服务器安装了wdcp管理面板,黑客完 ...
- 【JAVA8新的时间与日期 API】- 传统时间格式化的线程安全问题
Java8之前的日期和时间API,存在一些问题,最重要的就是线程安全的问题.这些问题都在Java8中的日期和时间API中得到了解决,而且Java8中的日期和时间API更加强大. 传统时间格式化的线程安 ...
- vue全家桶(2.4)
3.6.重定向和别名 3.6.1.重定向 路由重定向通俗的说就是从一个路由重新定位跳转到另一个路由,例如:访问的 "/a" 重定向到"/b" 重定向也是通过配置 ...
- C# @string $string $@string
@string 保证换行后也属于同一个字符串 (请特别注意\r\n这样也会直接输入,不在起到换行的效果) string execSql = @" SELECT T1.ProcInstID ...
- object detection api调参详解(兼SSD算法参数详解)
一.引言 使用谷歌提供的object detection api图像识别框架,我们可以很方便地重新训练一个预训练模型,用于自己的具体业务.以我所使用的ssd_mobilenet_v1预训练模型为例,训 ...
- 10大HBase常见运维工具整理
摘要:HBase自带许多运维工具,为用户提供管理.分析.修复和调试功能.本文将列举一些常用HBase工具,开发人员和运维人员可以参考本文内容,利用这些工具对HBase进行日常管理和运维. HBase组 ...
- oracle数据库插入日期格式字符串给date类型字段
首先执行 alter session set nls_date_format='yyyy-mm-dd hh24:mi:ss'; 然后执行insert语句而无需使用to_date函数