并发编程之CountDownLatch与CyclicBarrier

认识CountDownLatch

CountDownLatchjava.util.concurrent 包下面的多线程工具类
CountDownLatch用给定的计数初始化,await 方法阻塞,直到调用 countDown() 使当前计数为 0 时,所有处于等待状态的线程才会被释放。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用 CyclicBarrier

CountDownLatch 应用场景

典型的应用场景就是当一个服务启动时,同时会加载很多组件和服务,这时候主线程会等待组件和服务的加载。当所有的组件和服务都加载完毕后,主线程和其他线程在一起完成某个任务

CountDownLatch 还可以实现学生一起比赛跑步的程序,CountDownLatch 初始化为学生数量的线程,鸣枪后,每个学生就是一条线程,来完成各自的任务,当第一个学生跑完全程后,CountDownLatch 就会减一,直到所有的学生完成后,CountDownLatch 会变为 0 ,接下来再一起宣布跑步成绩

顺着这个场景,你自己就可以延伸、拓展出来很多其他任务场景

CountDownLatch 核心方法

await 方法

public void await() throws InterruptedException {
    
    
	sync.acquireSharedInterruptibly(1);
}

// 可以等待计数器一段时间再执行后续操作
public boolean await(long timeout, TimeUnit unit)
	throws InterruptedException {
    
    
	return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

await 方法会使当前线程在 CountDownLatch 计数减至 0 之前一直处于等待状态,除非线程被中断

await 方法内部会调用 acquireSharedInterruptibly 方法,这个 acquireSharedInterruptiblyAQS 中的方法,以共享模式进行中断

countDown 方法

public void countDown() {
    
    
	sync.releaseShared(1);
}

countDown 是和 await 同等重要的方法
countDown 方法的作用主要用来减小计数器的值,当计数器变为 0 时,在 CountDownLatchawait 的线程就会被唤醒,继续执行其他任务。当然也可以延迟唤醒,给 CountDownLatch 加一个延迟时间就可以实现

CountDownLatch 使用举例

举例

对于 CountDownLatch,其他线程为游戏玩家,比如英雄联盟,主线程为控制游戏开始的线程。在所有的玩家都准备好之前,主线程是处于等待状态的,也就是游戏不能开始。当所有的玩家准备好之后,下一步的动作实施者为主线程,即开始游戏。

我们使用代码模拟这个过程,我们模拟了三个玩家,在三个玩家都准备好之后,游戏才能开始

public class CountDownLatchTest {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch latch = new CountDownLatch(4);
        for(int i = 0; i < latch.getCount(); i++){
    
    
            new Thread(new MyThread(latch), "player"+i).start();
        }
        System.out.println("正在等待所有玩家准备好");
        latch.await();
        System.out.println("开始游戏");
    }

    private static class MyThread implements Runnable{
    
    
        private CountDownLatch latch ;

        public MyThread(CountDownLatch latch){
    
    
            this.latch = latch;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                Random rand = new Random();
                int randomNum = rand.nextInt((3000 - 1000) + 1) + 1000;//产生1000到3000之间的随机整数
                Thread.sleep(randomNum);
                System.out.println(Thread.currentThread().getName()+" 已经准备好了, 所使用的时间为 "+((double)randomNum/1000)+"s");
                latch.countDown();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

        }
    }
}

结果

正在等待所有玩家准备好
player0 已经准备好了, 所使用的时间为 1.235s
player2 已经准备好了, 所使用的时间为 1.279s
player3 已经准备好了, 所使用的时间为 1.358s
player1 已经准备好了, 所使用的时间为 2.583s
开始游戏

认识 CyclicBarrier

CyclicBarrier 也是 java.util.concurrent 包下面的多线程工具类
CyclicBarrier:利用 CyclicBarrier 类可以实现一组线程相互等待,当所有线程到达某个屏障点后再进行后续的操作。也就是让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞

CyclicBarrier 字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLockCondition 的组合使用

CyclicBarrierCountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic

在这里插入图片描述

CyclicBarrier 源码

CyclicBarrier 的源码实现和 CountDownLatch 大同小异,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的

CyclicBarrier 类的内部有一个计数器,每个线程在到达屏障点的时候都会调用 await 方法将自己阻塞,此时计数器会减 1,当计数器减为 0 的时候,所有因调用 await 方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理

CyclicBarrier 变量

//同步操作锁
private final ReentrantLock lock = new ReentrantLock();

//线程拦截器
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;
}

CyclicBarrier 构造函数

public CyclicBarrier(int parties, Runnable barrierAction) {
    
    
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
}

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

CyclicBarrier 内部维护了两个 int 型的变量 partiescount
parties 表示每次拦截的线程数,该值在构造时进行赋值
count 是内部计数器,它的初始值和 parties 相同,以后随着每次 await 方法的调用而减 1,直到减为 0 就将所有线程唤醒

CyclicBarrier 有一个静态内部类 Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand 表示换代前执行的任务,当 count 减为 0 时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定 barrierCommand 来执行自己的任务

在这里插入图片描述
await 方法

CyclicBarrier 类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待

// 非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
    
    
	  try {
    
    
	    return dowait(false, 0L);
	  } catch (TimeoutException toe) {
    
    
	    throw new Error(toe);
	  }
}
 
// 定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
    
    
  	return dowait(true, unit.toNanos(timeout));
}

可以看到不管是定时等待还是非定时等待,它们都调用了 dowait 方法,只不过是传入的参数不同而已。下面我们就来看看 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()) {
    
    
             //如果当前线程被中断会做以下三件事
      		 //1.打翻当前栅栏
      		 //2.唤醒拦截的所有线程
      		 //3.抛出中断异常
                breakBarrier();
                throw new InterruptedException();
            }
			
			// 每次都将计数器的值减 1
            int index = --count;
            // 计数器的值减为 0 则需唤醒所有线程并转换到下一代
            if (index == 0) {
    
      
                boolean ranAction = false;
                try {
    
    
                	// 唤醒所有线程前先执行指定的任务
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 唤醒所有线程并转到下一代
                    nextGeneration();
                    return 0;
                } finally {
    
    
                	// 确保在任务未成功执行时能将所有线程唤醒
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // 如果计数器不为 0 则执行此循环
            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();
        }
}

可以看到在 dowait 方法中每次都将 count1,减完后立马进行判断看看是否等于 0

  • 如果等于 0 的话就会先去执行之前指定好的任务,执行完之后再调用 nextGeneration 方法将栅栏转到下一代,在该方法中会将所有线程唤醒,将计数器的值重新设为 parties,最后会重新设置栅栏代次,在执行完
    nextGeneration 方法之后就意味着游戏进入下一局
  • 如果计数器此时还不等于 0 的话就进入 for 循环,根据参数来决定是调用 trip.awaitNanos(nanos) 还是 trip.await() 方法,这两方法对应着定时和非定时等待
  • 如果在等待过程中当前线程被中断就会执行 breakBarrier 方法,该方法叫做打破栅栏,意味着游戏在中途被掐断,设置 generationbroken 状态为 true 并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断整盘游戏就结束,所有之前被阻塞的线程都会被唤醒。线程醒来后会执行下面三个判断,看看是否因为调用 breakBarrier方法而被唤醒,如果是则抛出异常;看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;看看是否因为超时而被唤醒,如果是的话就调用 breakBarrier打破栅栏并抛出异常。这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒

CyclicBarrier 使用举例

对于 CyclicBarrier,假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程

public class CyclicBarrierTest {
    
    

    public static void main(String[] args) {
    
    
        CyclicBarrier barrier = new CyclicBarrier(3);
        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();
                }
            }
        }
    }
}

结果

main function is finished.
队友1, 通过了第0个障碍物, 使用了 1.432s
队友0, 通过了第0个障碍物, 使用了 1.465s
队友2, 通过了第0个障碍物, 使用了 2.26s
队友1, 通过了第1个障碍物, 使用了 1.542s
队友0, 通过了第1个障碍物, 使用了 2.154s
队友2, 通过了第1个障碍物, 使用了 2.556s
队友1, 通过了第2个障碍物, 使用了 1.426s
队友2, 通过了第2个障碍物, 使用了 2.603s
队友0, 通过了第2个障碍物, 使用了 2.784s

CountDownLatchCyclicBarrier

共同点

二者都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为 0 的时候所有阻塞的线程将会被唤醒

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行

不同点

  • 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待

  • CountdownLatch 利用继承 AQS 的共享锁来进行线程的通知
    CyclicBarrier 则利用 ReentrantLockCondition 来阻塞和通知线程

  • CyclicBarrier 支持重复使用,但是 CountDownLatch 不支持

猜你喜欢

转载自blog.csdn.net/weixin_38192427/article/details/113186677