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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值