一、 故事开始
时光匆匆,岁月不饶人,随着年龄的增长,我们肩上的担子越来越大了,除了本职工作,我们还想着通过其他途径再赚点儿外块,补贴家用(自己小金库)。
话说这天我的好友–小强,周末背【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(); } }
输出结果:
小强也是够苦的,三个人搬到他手上,他一个人搬到车上,对于我们做后端开发的何尝不是呢??
产品+前端+测试 对接我们后端开发 。
产品出个需求,前端写完代码要跟我们后端联调,测试催着我们赶快开发早点儿投入测试 我们后端只能跟小强一样吭哧吭哧的做。 做到最后发现手里的钱不足以支撑自己活下去呀。。。。
这个例子里用到了一个知识点,CyclicBarrier 字面意思是“循环栅栏”
使用方式上边已经写了,基本上就那样用,再总结一下他的具体作用,就是设置一个基础值parties(也就是小强一次能搬多少块砖) ,再给一个回调函数(也就是达到parties后小强需要做的事儿) ,然后就是await方法了(也就是A、B、C的工作 await一次就离回调更进一步)
二、知识的解析
我们不妨看一下源码
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(); }
关于await 和 awaitNanos 这是Lock 相关知识 这里不展开叙述
下边是一张流程图:
三、一些事
看博客经常看到拿CyclicBarrier 与 CountDownLatch 比较的 ,我个人觉得没什么比较的,如果如果看过我关于他俩的两篇文章,你就会全懂了。
四、 总结
深耕、广耕自己所在领域知识,争取让自己能靠自己所在领域的知识吃一辈子饭。
更多精彩关注公众号: