JUC(二):CyclicBarrier实现线程间互相等待

一、案例分析:

1.具体需求:

(1)假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。
(2)人满发车:长途汽车站提供长途客运服务,当等待坐车的乘客到达10人时,汽车站就会发出一辆长途汽车,让这10个乘客上车走人,等到下次等待的乘客又到达10人是,汽车站就会又发出一辆长途汽车
(3)模拟多线程分组计算:有一个大小为50000的随机数组,用5个线程分别计算10000个元素的和,然后在将计算结果进行合并,得出最后的结果

2.代码实现:

(1)需求一

package com.example.test;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class cyclicbarrierdemo {
        public static void main(String[] args) {
            CyclicBarrier barrier = new CyclicBarrier(3,()->{
                System.out.println("欢迎来到王者峡谷,所有队员成功翻越障碍!");
            });
            //创建3个(parties)个线程
            for(int i = 0; i < barrier.getParties(); i++){
                new Thread(new MyRunnable(barrier), "队友"+i).start();
            }
            System.out.println("main function is finished.");
        }

        private static class MyRunnable implements Runnable{
            private CyclicBarrier barrier;
            public MyRunnable(CyclicBarrier barrier){
                this.barrier = barrier;
            }
            
            @Override
            public void run() {
                for(int i = 0; i < 3; i++) {
                    try {
                        Random rand = new Random();
                        int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                        Thread.sleep(randomNum);
                        System.out.println(Thread.currentThread().getName() + ", 通过了第"+i+"个障碍物, 使用了 "+((double)randomNum/1000)+"s");
                        this.barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

(2)需求二完全类似需求1,自行实现
(3)需求三:

package com.example.test;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class cyclicbarrierCar {

    public static void main(String[] args) {
        //数组大小
        int size = 50000;
        //定义数组
        int[] numbers = new int[size];
        //随机初始化数组
        Random rand = new Random();
        for (int i = 0; i < size; i++) {
            numbers[i] = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
        }


        Long sum = 0L;
        for (int i = 0; i < size; i++) {
            sum += numbers[i];
        }
        //单线程计算结果
        System.out.println("单线程计算结果:" + sum);

        //多线程计算结果
        //定义线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //定义五个Future去保存子数组计算结果
        final int[] results = new int[5];

        //定义一个循环屏障,在屏障线程中进行计算结果合并
        CyclicBarrier barrier = new CyclicBarrier(5, () -> {
            int sums = 0;
            for (int i = 0; i < 5; i++) {
                sums += results[i];
            }
            System.out.println("多线程计算结果:" + sums);
        });

        //子数组长度
        int length = 10000;
        //定义五个线程去计算
        for (int i = 0; i < 5; i++) {
            //定义子数组
            int[] subNumbers = Arrays.copyOfRange(numbers, (i * length), ((i + 1) * length));
            //盛放计算结果
            int finalI = i;
            executorService.submit(() -> {
                for (int j = 0; j < subNumbers.length; j++) {
                    results[finalI] += subNumbers[j];
                }
                //等待其他线程进行计算
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }

        //关闭线程池
        executorService.shutdown();
    }

}

3.运行结果:

在这里插入图片描述
在这里插入图片描述

4.cyclicbarrier解读:

CyclicBarrier是java推出的一个并发编程工具,它用在多个线程之间协同工作。线程约定到达某个点,到达这个点之后的线程都停下来,直到最后一个线程也到达了这个点之后,所有的线程才会得到释放。常用的场景是:多个worker线程,每个线程都在循环地做一部分工作,并在最后用cyclicBarrier.await()设下约定点,当最后一个线程做完了工作也到达约定点后,所有线程得到释放,开始下一轮工作。也就是下面这样:

 while(!done()){
     //working
     cyclicBarrier.await();
 }

CyclicBarrier还支持一个回调函数,每当一轮工作结束后,下一轮工作开始前,这个回调函数都会被调用一次。

常用的方法

CyclicBarrier(int parties)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。

CyclicBarrier(int parties, Runnable barrierAction)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

int await()
在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。

int await(long timeout, TimeUnit unit)
在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。

int getNumberWaiting()
返回当前在屏障处等待的参与者数目。

int getParties()
返回要求启动此 barrier 的参与者数目。

boolean isBroken()
查询此屏障是否处于损坏状态。

void reset()
将屏障重置为其初始状态。如果调用了该函数,则在等待的线程将会抛出BrokenBarrierException异常。

关于构造器

//构造器1
    /** 创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待时触发,并在触发屏障时执行给定的屏障操作,由最后一个进入屏障的线程执行 */
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }


//构造器2
    /** 创建一个新的CyclicBarrier,当给定数量的参与方(线程)在等待它时,它将跳闸,并且在屏障跳闸时不执行预定义的操作 */
    public CyclicBarrier(int parties) {
        this(parties, null);
    }

关于成员变量

/** 同步操作锁 */
private final ReentrantLock lock = new ReentrantLock();
/** 线程拦截器 Condition维护了一个阻塞队列*/
private final Condition trip = lock.newCondition();
/** 每次拦截的线程数 */
private final int parties;
/* 换代前执行的任务 */
private final Runnable barrierCommand;
/** 表示栅栏的当前代 类似代表本局游戏*/
private Generation generation = new Generation();
/** 计数器 */
private int count;
/** 静态内部类Generation  */
private static class Generation {
boolean broken = false;

关于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 {
        final Generation g = generation;
        //检查当前栅栏是否被打翻
        if (g.broken)
            throw new BrokenBarrierException();
        //检查当前线程是否被中断
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        //每次都将计数器的值-1
        int index = --count;
        //计数器的值减为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();
            }
        }
        //如果计数器不为0 则执行此循环
        // 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 {
                    // 若在捕获中断异常前已经完成在栅栏上的等待,则直接调用中断操作
                    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();//最终解锁
    }
}

分两步分析,首先计数器的值减为0的情况,和计数器不为0的情况,首先第一种情况下:
在这里插入图片描述
第二种情况,计数器不为0,则进入自旋for(;;):
在这里插入图片描述
多线程同时并发访问,如何阻塞当前线程?
在这里插入图片描述
我们翻看源码,这里就看一下没有时间限制的【trip.await】方法:
在这里插入图片描述
整个await的过程:

1、将当前线程加入到Condition锁队列中。特别主要要区分AQS的等待队列,这里进入的是Condition的FIFO队列
2、释放锁。这里可以看到【fullyRelease】将锁释放了,否则【acquireQueued(node, savedState)】别的线程就无法拿到锁而发生死锁。
3、自旋(while)挂起,直到被唤醒或者超时或者CACELLED等。
4、获取锁【acquireQueued】方法,并将自己从Condition的FIFO队列中释放,表面自己不再需要锁(我已经有锁了)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值