一、 故事开始

时光匆匆,岁月不饶人,随着年龄的增长,我们肩上的担子越来越大了,除了本职工作,我们还想着通过其他途径再赚点儿外块,补贴家用(自己小金库)。

话说这天我的好友–小强,周末背【bèi】着媳妇儿去搬砖赚零花钱。

工地上有包工头的小侄子A、包工头的小姨子B、包工头的小叔子C和小强。

小强一看惊呆了,TMD一共四个人,就有三个关系户,这年头搬个砖也得靠关系吗?

没办发,为了赚点儿钱,小强还是硬着头皮准备干了。

包工头先进行了一项测试,测试小强的力气,看小强一次能搬多少块砖,经过测试小强只能一次搬三块,从砖窑搬到车上。

包工头开始分工了: A、B、C负责把砖放到小强手上,小强搬到车上,然后循环这个流程

好友程序员,跑去搬砖了_回调函数

时间过得很快,不知不觉一天过去了,小强满是疲惫的去找包工头结账,包工头给了他150元RMB,小强一看天色已晚,没有公交车回家了,只能打车,然后100块打了个车回家,回去为了犒劳自己除了顿好吃的,花了100,晚上回家算账发现一天不仅没有赚钱,还多支出了50。

越想越生气的小强拿出电脑,准备把今天的遭遇写一篇博客,来让广大网友们给自己点儿安慰。

于是他找到了他的朋友小明,来代笔帮他写一篇博客,身为技术人员的小明,当然不喜欢白话文写喽,直接把他的搬砖经历写成了代码。代码如下:

package com.example.democyclicbarrier;
import java.util.concurrent.CyclicBarrier;
/**
* @author 发现更多精彩 关注公众号:木子的昼夜编程
* 一个生活在互联网底层,做着增删改查的码农,不谙世事的造作
* @create 2021-08-15 10:07
*/
public class TestCyc {
public static void main(String[] args) {
System.out.println("小强准备赚钱...");
System.out.println("测试小强力气...");
// 重点关注代码
CyclicBarrier barr = new CyclicBarrier(3, ()->{
try {
System.out.println("够三块了,小强吭哧吭哧搬到车上去...");
Thread.sleep(2000);
} catch (Exception e) {
System.out.println("小强不干了...");
}
});
System.out.println("测试结果:一次能搬"+barr.getParties()+"块");
System.out.println("开始搬砖...");
// 假设一天搬5次
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try{
System.out.println("A放一块砖到小强手里.");
barr.await();
// 够三块了 才会执行下边代码逻辑
} catch (Exception e) {
System.out.println("A不干了");
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
System.out.println("B放一块砖到小强手里.");
barr.await();
// 够三块了 才会执行下边代码逻辑
} catch (Exception e) {
System.out.println("B不干了");
}
}
}).start();
new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
System.out.println("C放一块砖到小强手里.");
barr.await();
// 够三块了 才会执行下边代码逻辑
} catch (Exception e) {
System.out.println("C不干了");
}
}
}).start();
}
}

输出结果:

好友程序员,跑去搬砖了_后端开发_02

小强也是够苦的,三个人搬到他手上,他一个人搬到车上,对于我们做后端开发的何尝不是呢??

产品+前端+测试 对接我们后端开发 。

产品出个需求,前端写完代码要跟我们后端联调,测试催着我们赶快开发早点儿投入测试 我们后端只能跟小强一样吭哧吭哧的做。 做到最后发现手里的钱不足以支撑自己活下去呀。。。。

这个例子里用到了一个知识点,CyclicBarrier 字面意思是“循环栅栏”

使用方式上边已经写了,基本上就那样用,再总结一下他的具体作用,就是设置一个基础值parties(也就是小强一次能搬多少块砖) ,再给一个回调函数(也就是达到parties后小强需要做的事儿) ,然后就是await方法了(也就是A、B、C的工作 await一次就离回调更进一步)

好友程序员,跑去搬砖了_后端开发_03

二、知识的解析

我们不妨看一下源码

2.1 创建

// parties 设置的线程等待数 也就是需要几个await就会唤醒回调barrierAction执行
// barrierAction 回调 await的线程数到达parties个数就会执行这个回调
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
// 有人奇怪说这个count是干啥的 他就是一个中间量 先设置为parties的值
// 然后每次await count就会减去1 可以看上边那个图
// 等count等于0的时候 就执行回调以及---“改朝换代”
this.count = parties;
this.barrierCommand = barrierAction;
}

这个类有5个重要的属性需要关注:

// 锁 await的时候需要先获取到锁才能执行逻辑代码 
// 为什么需要锁,总不能十几个人一股脑都往小强手上放砖吧,那不直接出异常情况了嘛
// 没法区分谁是第一个 谁是最后一个 可能最后一个会执行多次
// 反正就是多个线程操作 count 一般情况都需要锁
private final ReentrantLock lock = new ReentrantLock();
// 这是lock锁的一个条件 所有线程都是通过condition的await来阻塞
// 等await数量足够了也就是count =0 的时候 所有线程会被signalAll
private final Condition trip = lock.newCondition();
// 这个不用多说了 就是await最大数量
private final int parties;
// 这个是回调 是一个Runnable
private final Runnable barrierCommand;
// 这个是一个代的概念,怎么说呢,就是小强每搬一次砖到车上都会new 一个新的Generation
// 来承载这次搬砖的信息 主要有一个参数是:broken 标记这一代是否被打断
private Generation generation = new Generation();
// 这个就是那个 勤勤恳恳的计数员 从parties到0 一直倒数计数
private int count;

2.2 await

//  返回index 也就是第几个await的线程,如果index = parties - 1 那就是第一个await的线程  
// 如果等于0 那就是最后一个await的线程 就会执行回调函数了
public int await() throws InterruptedException, BrokenBarrierException {
try {
//
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前代
final Generation g = generation;
// 如果当前代标记broken为true 那么直接抛异常 (不用重点关注)
if (g.broken)
throw new BrokenBarrierException();
// 如果线程被中断了 设置标记broken为true (不用重点关注
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 下边这是重点
// 1. count -- 如果结果是0 执行回调、改朝换代
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 执行回调: 就是执行barrierCommand这个Runnable
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 改朝换代:就是执行nextGeneration 主要有三个步骤
// 第一步:trip.signalAll(); 通知所有await的线程停止await继续往下执行
// 第二步:count = parties; 计数器从paeties重新开始倒数计数
// 第三部: generation = new Generation(); new 新的generation
nextGeneration();
return 0;
} finally {
// 任何异常 都直接终止这一代 也是三步:
// 第一步: generation.broken = true
// 第二步: count = parties;
// 第三部: trip.signalAll();
if (!ranAction)
breakBarrier();
}
}

// loop until tripped, broken, interrupted, or timed out
// 循环等待 被唤醒、这一代被broken、中断 、或者是超时(这个是await的有参方法,可以设置
// 等待超时时间)
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();
// 是否是当前代 这里很关键
// 一个线程可以使用多个栅栏 这里可能是被其他栅栏唤醒的 不理解这句话
// 我理解其实就是breakBarrier()这个方法signalAll的时候唤醒的 非本代的await的线程
// 如果不相等 说明已经改朝换代 直接返回index
// 如果没有改朝换代 那就是被别的代唤醒的,自己抓紧回去await等自己代唤醒
if (g != generation)
return index;
// 超时了
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
// 划重点 finally 解锁
lock.unlock();
}
}

几个小方法:

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

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

关于awaitawaitNanos 这是Lock 相关知识 这里不展开叙述

下边是一张流程图:

好友程序员,跑去搬砖了_后端开发_04

三、一些事

看博客经常看到拿CyclicBarrierCountDownLatch 比较的 ,我个人觉得没什么比较的,如果如果看过我关于他俩的两篇文章,你就会全懂了。

好友程序员,跑去搬砖了_后端开发_05