【并发编程】(十七)回环屏障CyclicBarrier使用及实现原理

1.CyclicBarrier概述

CountDownLatch类似,CyclicBarrier也可以看做是一个阻塞程序运行的计数器,我们可以定义一个计数器值,然后需要阻塞的线程可以通过await()方法来等待阻塞,直到调用await()方法的线程达到了我们定义的计数器值后,唤醒所有正在等待的线程。

1.1.CyclicBarrier和CountDownLatch的区别

比较维度CountDownLatchCyclicBarrier
使用方式定义了等待和计数器减1的方法,两个方法分开使用,更适合于一个线程等待多个线程的场景。定义了一个等待方法,这个方法组合了阻塞和计数器减1的功能,更适用于多个线程互相等待的场景。
是否复用不可以复用,使用一次之后需要重新创建CountDownLatch实例。可以复用,CyclicBarrier在创建时保存了一个计数器的常量,与一个计数器的变量,每次递减的是这个变量,在使用完后会将常量重新赋值给计数器变量,实现复用。
实现方式使用AQS共享锁,调用await()的线程会在共享锁队列中阻塞,直到其它线程调用countDown()将计数器减为0时唤醒。使用ReentrantLockCondition实现,线程调用await()方法会在临界区中对计数器做减操作,如果没有减到0,则将自己挂起在Condition队列中。最后一个进入的线程如果将计数器减到0,就调用signalAll()方法唤醒所有挂起的线程。

注:在网上查阅了部分资料都提到了CountDownLatch是做的减计数,而CyclicBarrier做的是加计数,不知道这个说法的依据来自哪里。从JDK8的源码中看,CyclicBarrier依然是做的减计数。

在这里插入图片描述

1.2.CyclicBarrier的使用

打开CyclicBarrier的使用API,可以看到它有两个构造方法,其中一个比较好理解,就是传入一个计数器值,用来配置可以阻塞多少个线程的。另外一个除了计数器值外,还可以传入一个Runnable的实例对象,这个对象可以在计数器值减到0后,发起一次调用。
例如:下面代码就会在计数器减到0后,打印出"回环屏障退出"。

CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("回环屏障退出"));

至于CyclicBarrier阻塞线程的使用方法也非常简单,线程调用一个await()方法就会将计数器减1,然后把自己阻塞。
下面的代码中,main线程就会和新创建的4个子线程互相等待,直到计数器减到0。

public static void test(CyclicBarrier cyclicBarrier) throws BrokenBarrierException, InterruptedException {
    for (int i = 0; i < 4; i++) {
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + ":开始等待");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + ":结束等待");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }, "线程" + i).start();
    }
    System.out.println(Thread.currentThread().getName() + ":开始等待");
    cyclicBarrier.await();
    System.out.println(Thread.currentThread().getName() + ":结束等待");
}

调用test()方法的打印结果如下:

线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程3:结束等待
线程1:结束等待
线程0:结束等待
main:结束等待
线程2:结束等待

可以看到,只有在5个线程都进入等待之后,才会退出回环屏障。


除此之外,CyclicBarrier是可以复用的,我们可以尝试一下直接调用两次,如下:

public static void main(String[] args) throws Exception {
    CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("回环屏障退出"));
    System.out.println("-----等一次-----");
    test(cyclicBarrier);
    System.out.println("-----等二次-----");
    test(cyclicBarrier);
}

打印结果可以证明CyclicBarrier确实是可以复用的。

-----等一次-----
线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程1:结束等待
线程3:结束等待
main:结束等待
线程0:结束等待
-----等二次-----
线程2:结束等待
线程0:开始等待
线程1:开始等待
线程2:开始等待
main:开始等待
线程3:开始等待
回环屏障退出
线程3:结束等待
线程1:结束等待
线程0:结束等待
main:结束等待
线程2:结束等待

2.CyclicBarrier实现原理

CyclicBarrier的阻塞和唤醒是通过ReentrantLockCondition来实现,如果想详细的看一下两者的实现原理,可以跳转到《(十四)Java可重入互斥锁实现——ReentrantLock详解》《(十五)Condition的使用及其阻塞唤醒原理》,本篇只对CyclicBarrier做分析。

2.1.CyclicBarrier的实例化

这里直接看构造方法,第一种只传入计数器数值。

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

可以看到它调用的是一个重载的构造方法,代码如下:

public CyclicBarrier(int parties, Runnable barrierAction) {
	// 显然,计数器值不能小于1
    if (parties <= 0) throw new IllegalArgumentException();
    // parties是一个常量,用在复用回环屏障时重置计数器值。
    this.parties = parties;
    // 当前的计数器值,线程的阻塞就是依据count值来判断的,每进入一个线程递减1
    this.count = parties;
    // 定义一个Runnable对象,当计数器归零时,会调用其run()方法
    this.barrierCommand = barrierAction;
}

有了实例化对象之后,就可以执行它的阻塞方法的了。

2.2.CyclicBarrier阻塞的实现

CyclicBarrier阻塞的实现本质是在调用Condition中的await()方法,这个方法可以选择是否传入等待时间,所以在CyclicBarrier中的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()就是回环屏障的核心实现代码了,这个方法比较长,所以下面的源码中省略一部分代码,只保留最重要的部分。

private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 回环屏障使用完毕或重置后,都会生成一个新的generation,这个对象可以用来让线程退出回环屏障
		final Generation g = generation;
		
       	// 每个进入的线程,都使计数器减1,当计数器归零后进入下面的if判断
        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
            	// 如果实例化时传入了Runnable对象,则在这里调用它的run()方法
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                // 里面做了唤醒所有等待线程的操作,线程是在下面的自旋中挂起的
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }
        
        for (;;) {
        	// 此处省略的线程被interrupt的try catch
        	// 根据是否传入等待时间来判断调用哪一个方法
	        if (!timed) {
	        	// condition的await()方法,这里会暂时释放锁
	            trip.await();
	        } else if (nanos > 0L) {
			    nanos = trip.awaitNanos(nanos);
			}
			
			// 计数器归零后,线程退出自旋
			if (g != generation) {
				return index;
			}                  
        }
    } finally {
        lock.unlock();
    }
}

上面代码中的trip就是一个Condition对象,是CyclicBarrier的一个成员变量,如下:

private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();

总结一下doWait()方法,其实做的事情还是比较简单的。

  • 线程进入doWait(), 先抢占到锁lock锁对象,并执行计数器递减1的操作。
  • 递减后的计数器值不为0,则将自己挂起在Condition队列中。
  • 递减后的计数器值为0,则调用signalAll()唤醒所有在条件队列中的线程,并创建新的generation对象,让线程可以退出回环屏障。

线程被唤醒后,是通过下面的判断来退出自旋的:

if (g != generation) {
	return index;
}   

关键点就在于这个generation对象是否已经被修改了,这个对象的修改是发生在nextGeneration()中的。

private void nextGeneration() {
    // 唤醒所有等待的线程
    trip.signalAll();
    // 重置计数器
    count = parties;
    // 创建新的generation对象,可以使上一轮的线程退出屏障,同时可以发起新一轮的屏障操作
    generation = new Generation();
}

2.3.小结

  • CyclicBarrier就是通过ReentrantLockCondition来实现阻塞和唤醒。
  • 通过在自己的成员变量中定义一个保存初始计数器值的常量,来重置当前的计数器值,并配合generation对象来重复使用屏障。
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值