一家人一起吃饭代码示例:
先定义一些方法,模拟吃饭场景
public static void fatherToRes() { System.out.println("爸爸步行去饭店需要3小时。"); } public static void motherToRes() { System.out.println("妈妈挤公交去饭店需要2小时。"); } public static void meToRes() { System.out.println("我乘地铁去饭店需要1小时。"); } public static void togetherToEat() { System.out.println("一家人到齐了,开始吃饭"); }
顺序执行:
public static void main(String[] args) { fatherToRes(); motherToRes(); meToRes(); togetherToEat(); } 输出: 爸爸步行去饭店需要3小时。 妈妈挤公交去饭店需要2小时。 我乘地铁去饭店需要1小时。 一家人到齐了,开始吃饭
我们发现,光集合就花了6个小时。改进版本:
public static void main(String[] args) { new Thread(() -> fatherToRes()).start(); new Thread(() -> motherToRes()).start(); new Thread(() -> meToRes()).start(); togetherToEat(); } 输出: 爸爸步行去饭店需要3小时。 一家人到齐了,开始吃饭 妈妈挤公交去饭店需要2小时。 我乘地铁去饭店需要1小时。
这个好像也不行,人还没到齐就开饭了。继续改进
//定义一个变量 必须等于0了才开饭 private static volatile int i = 3; public static void main(String[] args) { new Thread(() -> { fatherToRes(); i--; }).start(); new Thread(() -> { motherToRes(); i--; }).start(); new Thread(() -> { meToRes(); i--; }).start(); while (i != 0) { //此处一直hole住等待 } togetherToEat(); } 输出: 爸爸步行去饭店需要3小时。 妈妈挤公交去饭店需要2小时。 我乘地铁去饭店需要1小时。 一家人到齐了,开始吃饭
这个实际上达到了效果。但是,但是while盲等待是对于CPU的消耗太巨大了,我们需要更好的实现方式。(备注:此处用volatile修饰i是有并发问题的,读者可以使用AtomicInteger或者LongAdder改进,此处我就不改了哈)
可以参考:
【小家java】使用volatile关键字来实现内存可见性、实现轻量级锁
【小家java】AtomicLong可以抛弃了,请使用LongAdder代替(或使用LongAccumulator)
最终版本:
public static void main(String[] args) throws InterruptedException { new Thread(() -> { fatherToRes(); latch.countDown(); }).start(); new Thread(() -> { motherToRes(); latch.countDown(); }).start(); new Thread(() -> { meToRes(); latch.countDown(); }).start(); latch.await(); togetherToEat(); } 输出: 妈妈挤公交去饭店需要2小时。 爸爸步行去饭店需要3小时。 我乘地铁去饭店需要1小时。 一家人到齐了,开始吃饭
这样子,我们就不用一直hold住cpu,不用盲等了。
CyclicBarrier
CyclicBarrier概念,前言里面已经有所介绍了。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
public CyclicBarrier(int parties) { this(parties, null); }
我们把上面一家人一起吃饭的例子改造一下:
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(4); public static void main(String[] args) throws Exception { new Thread(() -> { fatherToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); new Thread(() -> { motherToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); new Thread(() -> { meToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); //主线程也要await cyclicBarrier.await(); togetherToEat(); }
需要注意的是:
1、main主线程也是一个线程,所以也要await
2、new CyclicBarrier的值是4,而不是3(会有个线程控制不了),也不是5(程序将永远等待,因为没有第五个线程执行await方法,即没有第五个线程到达屏障,所以之前到达屏障的四个线程都不会继续执行。)
CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时(前提条件也必须是先达到屏障),优先执行barrierAction,方便处理更复杂的业务场景。(比如一家人吃饭,必须永远是爸爸先吃)
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> System.out.println("人到齐了,爸爸先动筷子")); public static void main(String[] args) throws Exception { new Thread(() -> { fatherToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); new Thread(() -> { motherToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); new Thread(() -> { meToRes(); try { cyclicBarrier.await(); } catch (Exception e) { } }).start(); //主线程也要await cyclicBarrier.await(); togetherToEat(); } 输出: 爸爸步行去饭店需要3小时。 妈妈挤公交去饭店需要2小时。 我乘地铁去饭店需要1小时。 人到齐了,爸爸先动筷子 一家人到齐了,开始吃饭
应用场景
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。