CyclicBarrier是一个用于线程同步的辅助类,它允许一组线程等待彼此,直到所有线程都到达集合点,然后执行某个设定的任务。

现实中有个很好的例子来形容:几个人约定了某个地方集中,然后一起出发去旅行。每个参与的人就是一个线程,CyclicBarrier就是那个集合点,所有人到了之后,就一起出发。

CyclicBarrier的构造函数有两个:

// parties是参与等待的线程的数量,barrierAction是所有线程达到集合点之后要做的动作
public CyclicBarrier(int parties, Runnable barrierAction); // 达到集合点之后不执行操作的构造函数
public CyclicBarrier(int parties)

需要说明的是,CyclicBarrier只是记录线程的数目,CyclicBarrier是不创建任何线程的。线程是通过调用CyclicBarrier的await方法来等待其他线程,如果调用await方法的线程数目达到了预设值,也就是上面构造方法中的parties,CyclicBarrier就会开始执行barrierAction。

因此我们来看CyclicBarrier的核心方法dowait,也就是await方法调用的私有方法:

    private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation; if (g.broken)
throw new BrokenBarrierException(); if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// count就是预设的parties,count减1的值表示还剩余几个
// 线程没有达到该集合点
int index = --count;
// index为0表示所有的线程都已经达到集合点,这时
// 占用最后一个线程,执行运行设定的任务
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 唤醒其他等待的线程,
// 更新generation以便下一次运行
nextGeneration();
return 0;
} finally {
// 如果运行任务时发生异常,设置状态为broken
// 并且唤醒其他等待的线程
if (!ranAction)
breakBarrier();
}
} // 还有线程没有调用await,进入循环等待直到其他线程
// 达到集合点或者等待超时
for (;;) {
try {
// 如果没有设置超时,进行无超时的等待
if (!timed)
trip.await();
// 有超时设置,进行有超时的等待
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// generation如果没有被更新表示还是当前的运行
// (generation被更新表示集合完毕并且任务成功),
// 在状态没有被设置为broken状态的情况下,遇到线程
// 中断异常表示当前线程等待失败,需要设置为broken
// 状态,并且抛出中断异常
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// else对应的条件为:g != generation || g.broken
// 表示要么generation已经被更新意味着所有线程已经到达
// 集合点并且任务执行成功,要么就是是broken状态意味着
// 任务执行失败,无论哪种情况所有线程已经达到集合点,当
// 前线程要结束等待了,发生了中断异常,需要中断当前线程
// 表示遇到了中断异常。
Thread.currentThread().interrupt();
}
} // 如果发现当前状态为broken,抛出异常
if (g.broken)
throw new BrokenBarrierException();
// generation被更新表示所有线程都已经达到集合点
// 并且预设任务已经完成,返回该线程进入等待顺序号
if (g != generation)
return index;
// 等待超时,设置为broken状态并且抛出超时异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

1. 任何一个线程等待时发生异常,CyclicBarrier都将被设置为broken状态,运行都会失败

2. 每次运行成功之后CyclicBarrier都会清理运行状态,这样CyclicBarrier可以重新使用

3. 对于设置了超时的等待,在发生超时的时候会引起CyclicBarrier的broken

说完了CyclicBarrier,再来说说CountDownLatch。

CountDownLatch同样也是一个线程同步的辅助类,同样适用上面的集合点的场景来解释,但是运行模式完全不同。

CyclicBarrier是参与的所有的线程彼此等待,CountDownLatch则不同,CountDownLatch有一个导游线程在等待,每个线程报到一下即可无须等待,等到导游线程发现所有人都已经报到了,就结束了自己的等待。

CountDownLatch的构造方法允许指定参与的线程数量:

public CountDownLatch(int count)

参与线程使用countDown表示报到:

    public void countDown() {
sync.releaseShared(1);
}

看到releaseShared很容易使人联想到共享锁,那么试着用共享锁的运行模式来解释就简单得多了:

和信号量的实现类似,CountDownLatch内置一下有限的共享锁。

每个参与线程拥有一把共享锁,调用countDown就等于是释放了自己的共享锁,导游线程await等于一下子要拿回所有的共享锁。那么基于AbstractQueuedSynchronizer类来实现就很简单了:

    public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

在await时注意到数量是1,其实这个参数对于CountDownLatch实现的Sync类(AbstractQueuedSynchronizer的子类)来说是不起作用的,因为需要保证await获取共享锁时必须拿到所有的共享锁,这个参数也就变得没有意义了。看一下Sync的tryAcquireShared方法就明白了:

        protected int tryAcquireShared(int acquires) {
// 和信号量Semaphore的实现一样,使用state来存储count,
// 每次释放共享锁就把state减1,state为0表示所有的共享
// 锁已经被释放。注意:这里的acquires参数不起作用
return (getState() == 0) ? 1 : -1;
}

因此Sync的tryReleaseShared就是更新state(每次state减1):

        protected boolean tryReleaseShared(int releases) {
// 每次state减1,当state为0,返回false表示所有的共享锁都已经释放
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

CyclicBarrier和CountDownLatch本质上来说都是多个线程同步的辅助工具,前者可以看成分布式的,后者可以看出是主从式。

《java.util.concurrent 包源码阅读》21 CyclicBarrier和CountDownLatch的更多相关文章

  1. 《java.util.concurrent 包源码阅读》 结束语

    <java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...

  2. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  3. 《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包

    Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的A ...

  4. 《java.util.concurrent 包源码阅读》04 ConcurrentMap

    Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:Concu ...

  5. 《java.util.concurrent 包源码阅读》17 信号量 Semaphore

    学过操作系统的朋友都知道信号量,在java.util.concurrent包中也有一个关于信号量的实现:Semaphore. 从代码实现的角度来说,信号量与锁很类似,可以看成是一个有限的共享锁,即只能 ...

  6. 《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue

    对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元 ...

  7. 《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇

    concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void ...

  8. 《java.util.concurrent 包源码阅读》05 BlockingQueue

    想必大家都很熟悉生产者-消费者队列,生产者负责添加元素到队列,如果队列已满则会进入阻塞状态直到有消费者拿走元素.相反,消费者负责从队列中拿走元素,如果队列为空则会进入阻塞状态直到有生产者添加元素到队列 ...

  9. 《java.util.concurrent 包源码阅读》10 线程池系列之AbstractExecutorService

    AbstractExecutorService对ExecutorService的执行任务类型的方法提供了一个默认实现.这些方法包括submit,invokeAny和InvokeAll. 注意的是来自E ...

随机推荐

  1. web容器启动后自动执行程序的几种方式比较

    1.       背景 1.1.       背景介绍 在web项目中我们有时会遇到这种需求,在web项目启动后需要开启线程去完成一些重要的工作,例如:往数据库中初始化一些数据,开启线程,初始化消息队 ...

  2. Linux系列教程(二)——Linux系统安装(手把手学安装centos6.8)

    在上一篇博客我们简单的介绍了Linux系统的起源,这篇博客我们将通过图示一步一步教大家如何安装Linux系统.注意这里我们选择安装的Linux系统是其一种发行版本 CentOS,这里给大家普及一个概念 ...

  3. [extjs(1)]MyEclipse2014安装ext4插件Spket

    1 解压好的Spket目录如下 2  建议以link方式安装Spket到MyEclipse中 找到MyEclipse的安装目录 如 3  在MyEclipse 的根目录新建一个目录extjs 当然也可 ...

  4. 自学ConcuurentHashMap源码

    自学ConcuurentHashMap源码 参考:https://my.oschina.net/hosee/blog/675884 http://www.cnblogs.com/ITtangtang/ ...

  5. MySQL复制之理论篇

    一.MySQL复制概述 MySQL支持两种复制方式:基于行的复制和基于语句的复制(逻辑复制).这两种方式都是通过在主库上记录 二进制日志.在备库重放日志的方式来实现异步的数据复制,其工作原理如下图: ...

  6. Vue源码后记-钩子函数

    vue源码的马拉松跑完了,可以放松一下写点小东西,其实源码讲20节都讲不完,跳了好多地方. 本人技术有限,无法跟大神一样,模拟vue手把手搭建一个MVVM框架,然后再分析原理,只能以门外汉的姿态简单过 ...

  7. ubuntu 常用软件安装

    安装ubuntu远程图形界面 sudo apt-get install xrdp (sudo apt-get install ..  用于安装软件的命令 ) sudo apt-get install ...

  8. 【JAVA零基础入门系列】Day12 Java类的简单应用

    俗话说的好,实践出真知,所以除了理论知识掌握扎实以外,更重要的是要多加操练,这样才能掌握核心科技. 今天我们就用刚学会的类来实践一下,目标便是完成上一篇中的剁手任务. 我们的商品类已经准备好了,代码重 ...

  9. poj 1759 Garland

    Garland Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 2365   Accepted: 1007 Descripti ...

  10. 针对数据量较大的表,需要进行跨库复制,采用navcat 实现sqlite数据库跨数据库的数据表迁移 [转载]

    2014年12月13日 14:36 新浪博客 (转自http://www.cnblogs.com/nmj1986/archive/2012/09/17/2688827.html) 需求: 有两个不同的 ...