CyclicBarrier源码解析

和CountDownLatch不同,CyclicBarrier表示调用await方法之后,线程会阻塞知道,知道一定数目的线程执行了await之后才会全部唤醒。而且CyclicBarrier还有两个功能点,支持重复使用和唤醒时回调。

所以我们需要第一步了解到CyclicBarried是如何等待多个线程等待并且一起唤醒的。

1、初始化CyclicBarrier

CyclicBarrier默认的初始化比较简单,parties表示需要等待多少个线程await后才唤醒。

    public CyclicBarrier(int parties) {
        this(parties, null);
    }

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

CyclicBarrier同样支持在唤醒的时候增加某个方法特殊处理,称为barriedAction,是一个实现了Runnable的类型,默认不传时为null。

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }

2、线程等待await方法

await支持无参和传参两种方式,传参表示允许线程调用await的超时时间

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

在了解dowait方法的时候我们需要了解一个东西,Generation

    private static class Generation {
        boolean broken = false;
    }

    private Generation generation = new Generation();

每初始化一个CyclicBarrier,内部都会有一个Generation的对象。CyclicBarrier的每次使用都表示为一个生成实例。每当CyclicBarrier被触发时(有 parties 个线程全部到达 barrier 时),Generation都会发生变化,或者重置。重置后的CyclicBarrier可以继续使用,这点和CountDownLatch不太一样。

There can be many generations associated with threads using the barrier - due to the non-deterministic way the lock may be allocated to waiting threads - but only one of these can be active at a time and all the rest are either broken or tripped.(大概意思是说,可能会有很多线程以及对应多个Generation,因为没有确定的方式去分别锁给这些线程。但是只会有一个在用的Generation,其他不是被重置就是broken)

所以这里有个问题,Generation内部只会有一个 broken 表示 当前Generation时否重置,在没有版本号之类的其他域情况下如何保证每一轮的线程不是同一个Generation。

回到dowait方法解析

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

            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;

                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

从上面的逻辑我们大概可以抽出最基本的逻辑为

  1. int index = --count; 获取当前时第几个线程;
  2. 如果index 不为0,调用ReentrantLock.Condition 的await方法,将当前线程放入条件队列中挂起;
  3. 如果index 已为0,调用ReentrantLock.Condition 的signalAll方法,将等待队列中的节点全部唤醒。

所以CyclicBarrier并没有像前面提到的CountDownLatch、ReentrantLock通过内部类Sync实现AQS,而是通过调用ReentrantLock的Condition 将任务放入到条件队列中,等待parties为0时,全部唤醒。

这里我们开始一点点分析dowait的实现

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

          .....
        } finally {
            lock.unlock();
        }
    }
  1. 前面涉及到一些状态的初始化和加锁,因为条件队列的等待需要在ReentrantLock.lock和unlock中,所以之里再try-finally分别涉及到对ReentrantLock的加锁和释放锁。备注一句因为能够进入条件队列的节点Node必然是在ReentrantLock中获取到锁的。
  2. 当前线程调用CyclicBarrier.await(),并获取当前的Generation,如果当前的Generation的broken 为true,表示当前CyclicBarrier已经被破坏,需要抛出异常BrokenBarriedException;
  3. 考虑到使用CyclicBarrier的地方,调用CyclicBarrier.await的数目应该是等于 初始化CyclicBarrier的parites数目,所以这里如果其中一个线程是中断的(Thread.interrupted为true),需要破坏当前CyclicBarrier,所以之里调用了breakBarried方法,并抛出异常。
    // 1、设置当前Generation 的broken值为true
    // 2、设置count的值为初始化parties
    // 3、唤醒等待队列中的线程节点
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

breakBarrier除了上面注释提到的3点,还有两个地方需要记住

    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;
  1. count初始值为传递给CyclicBarrier的parties值,在breakBarried中 会被初始化为parties;
  2. 因为其中一个线程执行失败而调用breakBarrier,需要把这个信息传递给在等待队列中唤醒的线程(重点:generation.broken = true)。

接着来看index 为0,表示当前线程调用await时,需要唤醒等待队列中的线程

            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

前面说了CyclicBarrier允许在构造时传入参数Runnable,表示需要在唤醒等待队列中的节点前执行的操作。注意是Runnable并没有新起一个线程执行,而是在当前index 为0的线程执行。

具体的唤醒等待队列的节点,并且重置Generation和count是在nextGeneration方法执行。

    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();   // 唤醒等待队列中的全部节点
        // set up next generation
        count = parties;
        generation = new Generation();
    }

考虑到Runnable可能有执行失败的可能,所以这里在执行失败的时候破坏屏障CyclicBarrier

        finally {
           if (!ranAction)
              breakBarrier();
        }

下面就需要考虑不需要唤醒队列中节点时,如何进入等待队列过程

            // 循环直到以下条件满足
            // 1、Generation为新的值,也就是换代
            // 2、Generation.broken = true,也就是broken
            // 3、当前线程中断
            // 4、超时发生
            for (;;) {
                try {
                    // 如果没有设置超时时间
                    if (!timed)
                        // 调用等待队列默认的await方法
                        trip.await();
                    else if (nanos > 0L)
                        // 否则调用带有超时时间的等待方法,返回值时剩余时间
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    // 如果当前线程因为中断被停止等待
                    // 但是在等待过程中,其并不知道Generation是否被换代了
                    // 或者broken为true,所以唤醒的同时需要检查下
                    // 正常无论时count为0,或者说breakBarrier,
                    // 都是调用notifyAll唤醒全部线程,这里的InterruptException
                    // 我个人更加认为是线程自己发出的,而不关CyclicBarrier
                    // 因而不知道Generation当前现状,需要判断下
                    if (g == generation && ! g.broken) {
                        // 如果没有换代,而且Generation.broken仍然为false,
                        // 考虑到调用CyclicBarrier.await的数目应该是等于 
                        // 初始化CyclicBarrier的parites数目,所以避免其他线程无法唤醒
                        // 这里breakBarrier唤醒其他线程,并抛出异常
                        breakBarrier();
                        throw ie;
                    } else {
                        // 即使我们没有被中断,我们也将完成等待,
                        // 因此该中断被视为“属于”后续执行。
                        // 走到这里应该是换代,或者CyclicBarrier被打破了
                        // 设置中断标志位,应该是为了记录线程被唤醒了
                        // 换代或者打破屏障应该是交给下面去判断了
                        Thread.currentThread().interrupt();
                    }
                }

                // 线程唤醒,但是发现屏障被打破了,抛出BrokenBarrierException异常
                if (g.broken)
                    throw new BrokenBarrierException();
                // 唤醒后发现换代了,立即返回并返回当前是第几个执行await的
                if (g != generation)
                    return index;
                // 超时了的话也打破屏障 并返回超时异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }

在dowait中可以根据返回类型判断是因为什么原因返回的

  • BrokenBarrierException 屏障被打破,一般是因为同一个Generation中,broken为true造成的,有可能是因为当代Generation中某个线程(也许是当前线程)除了异常导致需要打破当前Generation;
  • InterruptException 当前线程被中断,并在此之前调用breakBarrier唤醒其他线程
  • TimeoutException 超时返回
  • 正常返回当前是第几个调用await的线程,或者说还差几个就能唤醒

3、每个线程如何直到本轮Generation已经结束

每一轮都会生成一个新的Generation,当每个线程在dowait方法中发现自己持有的generation不为 当前的全局的Generation时,将会认为当前自己的generation已经结束。

4、关于Generation的理解

前面提到,每一轮都会生成一个新的Generation,确保只有一个年代的CyclicBarrier。那如何确保其他的年代不会有影响呢。

首先开启一个新的Generation,有两种原因,上一轮Generation已经顺利结束,或者上一轮Generation被动结束,所以不得不执行breakBarrier,同时为了让其他线程知道本轮Generation被打破,所以把Generation.broken = true。这也说明了Generation本身只有一个broken一个参数的原因,因为不再需要其他参数。

4.1、我们先来看上一轮Generation正常结束的情况,具体在dowait的方法内部,当index 为0时

       ........
       if (index == 0) {  // tripped
            boolean ranAction = false;
        try {
            final Runnable command = barrierCommand;
        if (command != null)
            command.run();
            ranAction = true;
            nextGeneration();
            return 0;
        } finally {
           if (!ranAction)
              breakBarrier();
        }
    }

dowait方法中,一旦确定本线程时最后一个线程之后,就可以调用Runnable方法,并且唤醒等待队列中的线程。注意时先执行Runnable方法再执行唤醒队列中的节点,所以这里有可能执行Runnable方法失败的可能,一旦失败就需要打破当前Generation。

    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

正常结束本轮Generation,调用nextGeneration方法,唤醒等待队列的节点,重新生成Generation并重置count为初始化parties,关于partiest不能重置,所以这里初始化设定就不能更改parties。

从这里也可以看出来,每一个线程在执行await方法->doawait方法时,都会获取当前Generation的引用,也就是让每个Generation引用和线程产生联系,反过来说线程在执行dowait的生命周期中,只会和一个Generation有联系。

4.2、当出现一个线程执行异常的时候,会调用brokeGeneration方法破坏当前Generation,这里出现的地方比较多

// dowait方法中当前线程被中断
if( Thread.interrupted()) {
	breakBarrier();
	thrownew  InterruptedException();
}

// dowait方法中因为执行Runnable方法失败
finally {
   if (!ranAction)
      breakBarrier();
}

// dowait方法中,等待队列的节点因为InterruptException唤醒
    if (!timed)
		trip.await();
	else if (nanos > 0L)
		nanos = trip.awaitNanos(nanos);
	} catch (InterruptedException ie) {
		if (g == generation && ! g.broken) {
			breakBarrier();
			throw ie;
		} else {
			Thread.currentThread().interrupt();
		}
	}

// 调用方法reset
   public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }

// breakGeneration
    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

就比较明显,可能会因为某些原因,例如其中一个线程异常、Runnable执行异常或者被重置的原因,就需要打破当前的Barrier。同时为了让其他等待队列中的线程知道时因为当前Barrier时被打碎的,就需要把broken 设置为true。并且在breakBarrier中唤醒其他线程(trip.signalAll)。其他线程发现自己持有的Generation带有的broken 域为true,就会抛出BrokenBarrierException。

所以收broken 这个域,更多的是让等待队列中的线程唤醒后检查自身唤醒的原因,如果是正常结束本轮Generation,该值为false,否则为true!!!

除此之外CyclicBarrier 还有一点很重要,只要有一个线程出现异常,就会打碎当前Barrier,唤醒其他线程,这里dowait很多地方都可以看到。

            // dowait方法的209行, 什么都没执行的时候因为标志位也立即打碎
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
 
            // dowait方法232行,等待队列中的节点因为中断被唤醒,那么认为当前线程除了问题
            // 也立即打碎屏障
                try {
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
龙果 java并发编程原理实战 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解00:21:56分钟 | 第6节线程的初始化,中断以及其源码讲解00:21:26分钟 | 第7节多种创建线程的方式案例演示(一)带返回值的方式00:17:12分钟 | 第8节多种创建线程的方式案例演示(二)使用线程池00:15:40分钟 | 第9节Spring对并发的支持:Spring的异步任务00:11:10分钟 | 第10节使用jdk8提供的lambda进行并行计算00:14:22分钟 | 第11节了解多线程所带来的安全风险00:13:16分钟 | 第12节从线程的优先级看饥饿问题00:18:42分钟 | 第13节从Java字节码的角度看线程安全性问题00:25:43分钟 | 第14节synchronized保证线程安全的原理(理论层面)00:13:59分钟 | 第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题与线程安全性深入解析00:27:15分钟 | 第17节理解自旋锁,死锁与重入锁00:24:58分钟 | 第18节深入理解volatile原理与使用00:28:30分钟 | 第19节JDK5提供的原子类的操作以及实现原理00:27:10分钟 | 第20节Lock接口认识与使用00:19:54分钟 | 第21节手动实现一个可重入锁00:26:31分钟 | 第22节AbstractQueuedSynchronizer(AQS)详解00:49:04分钟 | 第23节使用AQS重写自己的锁00:31:04分钟 | 第24节重入锁原理与演示00:12:24分钟 | 第25节读写锁认识与原理00:18:04分钟 | 第26节细读ReentrantReadWriteLock源码00:30:38分钟 | 第27节ReentrantReadWriteLock锁降级详解00:13:32分钟 | 第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第32节使用Condition重写wait/notify案例并实现一个有界队列00:22:05分钟 | 第33节深入解析Condition源码00:21:15分钟 | 第34节实战:简易数据连接池00:24:53分钟 | 第35节线程之间通信之join应用与实现原理剖析00:10:17分钟 | 第36节ThreadLocal 使用及实现原理00:17:41分钟 | 第37节并发工具类CountDownLatch详解00:22:04分钟 | 第38节并发工具类CyclicBarrier 详解00:11:52分钟 | 第39节并发工具类Semaphore详解00:17:27分钟 | 第40节并发工具类Exchanger详解00:13:47分钟 | 第41节CountDownLatch,CyclicBarrier,Semaphore源码解析00:29:57分钟 | 第42节提前完成任务之FutureTask使用00:11:43分钟 | 第43节Future设计模式实现(实现类似于JDK提供的Future)00:19:20分钟 | 第44节Future源码解读00:29:22分钟 | 第45节Fork/Join框架详解00:28:09分钟 | 第46节同步容器与并发容器00:18:44分钟 | 第47节并发容器CopyOnWriteArrayList原理与使用00:15:52分钟 | 第48节并发容器ConcurrentLinkedQueue原理与使用00:31:03分钟 | 第49节Java中的阻塞队列原理与使用00:26:18分钟 | 第50节实战:简单实现消息队列00:11:07分钟 | 第51节并发容器ConcurrentHashMap原理与使用00:38:22分钟 | 第52节线程池的原理与使用00:42:49分钟 | 第53节Executor框架详解00:36:54分钟 | 第54节实战:简易web服务器(一)00:55:34分钟 | 第55节实战:简易web服务器(二)00:24:36分钟 | 第56节JDK8的新增原子操作类LongAddr原理与使用00:17:45分钟 | 第57节JDK8新增锁StampedLock详解00:29:37分钟 | 第58节重排序问题00:23:19分钟 | 第59节happens-before简单概述00:15:17分钟 | 第60节锁的内存语义00:13:54分钟 | 第61节volatile内存语义00:12:04分钟 | 第62节final域的内存语义00:34:07分钟 | 第63节实战:问题定位00:07:48分钟
第1节你真的了解并发吗? [免费观看][免费观看] 00:27:48分钟 | 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解00:21:56分钟 | 第6节线程的初始化,中断以及其源码讲解00:21:26分钟 | 第7节多种创建线程的方式案例演示(一)带返回值的方式00:17:12分钟 | 第8节多种创建线程的方式案例演示(二)使用线程池00:15:40分钟 | 第9节Spring对并发的支持:Spring的异步任务00:11:10分钟 | 第10节使用jdk8提供的lambda进行并行计算00:14:22分钟 | 第11节了解多线程所带来的安全风险00:13:16分钟 | 第12节从线程的优先级看饥饿问题00:18:42分钟 | 第13节从Java字节码的角度看线程安全性问题00:25:43分钟 | 第14节sy nchronized保证线程安全的原理(理论层面)00:13:59分钟 | 第15节synchronized保证线程安全的原理(jvm层面)00:25:03分钟 | 第16节单例问题与线程安全性深入解析00:27:15分钟 | 第17节理解自旋锁,死锁与重入锁00:24:58分钟 | 第18节深入理解volatile原理与使用00:28:30分钟 | 第19节JDK5提供的原子类的操作以及实现原理00:27:10分钟 | 第20节Lock接口认识与使用00:19:54分钟 | 第21节手动实现一个可重入锁00:26:31分钟 | 第22节AbstractQueuedSynchronizer(AQS)详解00:49:04分钟 | 第23节使用AQS重写自己的锁00:31:04分钟 | 第24节重入锁原理与演示00:12:24分钟 | 第25节读写锁认识与原理00:18:04分钟 | 第26节细读ReentrantReadWriteLock源码00:30:38分钟 | 第27节ReentrantReadWriteLock锁降级详解00:13:32分钟 | 第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程之间的通信之wait/notify00:32:12分钟 | 第30节通过生产者消费者模型理解等待唤醒机制00:20:50分钟 | 第31节Condition的使用及原理解析00:17:40分钟 | 第32节使用Condition重写wait/notify案例并实现一个有界队列00:22:05分钟 | 第33节深入解析Condition源码00:21:15分钟 | 第34节实战:简易数据连接池00:24:53分钟 | 第35节线程之间通信之join应用与实现原理剖析00:10:17分钟 | 第36节ThreadLocal 使用及实现原理00:17:41分钟 | 第37节并发工具类CountDownLatch详解00:22:04分钟 | 第38节并发工具类CyclicBarrier 详解00:11:52分钟 | 第39节并发工具类Semaphore详解00:17:27分钟 | 第40节并发工具类Exchanger详解00:13:47分钟 | 第41节CountDownLatch,CyclicBarrier,Semaphore源码解析00:29:57分钟 | 第42节提前完成任务之FutureTask使用00:11:43分钟 | 第43节Future设计模式实现(实现类似于JDK提供的Future)00:19:20分钟 | 第44节Future源码解读00:29:22分钟 | 第45节Fork/Join框架详解00:28:09分钟 | 第46节同步容器与并发容器00:18:44分钟 | 第47节并发容器CopyOnWriteArrayList原理与使用00:15:52分钟 | 第48节并发容器ConcurrentLinkedQueue原理与使用00:31:03分钟 | 第49节Java中的阻塞队列原理与使用00:26:18分钟 | 第50节实战:简单实现消息队列00:11:07分钟 | 第51节并发容器ConcurrentHashMap原理与使用00:38:22分钟 | 第52节线程池的原理与使用00:42:49分钟 | 第53节Executor框架详解00:36:54分钟 | 第54节实战:简易web服务器(一)00:55:34分钟 | 第55节实战:简易web服务器(二)00:24:36分钟 | 第56节JDK8的新增原子操作类LongAddr原理与使用00:17:45分钟 | 第57节JDK8新增锁StampedLock详解00:29:37分钟 | 第58节重排序问题00:23:19分钟 | 第59节happens-before简单概述00:15:17分钟 | 第60节锁的内存语义00:13:54分钟 | 第61节volatile内存语义00:12:04分钟 | 第62节final域的内存语义00:34:07分钟 | 第63节实战:问题定位00:07:48分钟 |

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值