JUC下的CountDownLatch,CyclicBarrier、Semaphore的使用方法

countDownLatch是在 java1.5 被引入,跟它一起被引入的工具类还有 CyclicBarrier、Semaphore、concurrentHashMap 和 BlockingQueue,它们存在于java.util.cucurrent包下,这里我们只会说到countDownLatch、CyclicBarrier、Semaphore这三种。

什么是CountDownLatch

意思是 CountDownLatch 是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。

CountDownLatch方法列表

   public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

传入一个 count 值,构造一个用给定计数初始化的 CountDownLatch。

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

await方法使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

await(long timeout, TimeUnit unit)方法使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间

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

countDown方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程。

    public long getCount() {
        return sync.getCount();
    }

getCount方法返回当前计数。

而常用的方法有:

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

当一个线程调用 await 方法时,就会阻塞当前线程。每当有线程调用一次 countDown 方法时,计数就会减 1。当 count 的值等于 0 的时候,被阻塞的线程才会继续运行。

我们举个例子,比如外面小哥张三接到了两个很紧急的单子,但是如果一个人送单时间会赶不上,这时叫来了另一个外面小哥李四,一人送一个单子,最后两个单子都在规定的时间内完成了。

现在我们来代码模拟一下:

static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        
        Worker worker1 = new Worker("外面小哥张三", 10000, countDownLatch);
        
        Worker worker2 = new Worker("外面小哥李四", 10000, countDownLatch);
        
        worker1.start();
        
        worker2.start();

        long startTime = System.currentTimeMillis();
        
        countDownLatch.await();
        
        System.out.println("外卖单子全部送完,结束时间:"+ (System.currentTimeMillis() - startTime));

    }

    static class Worker extends Thread{
    	
        String name;
        
        int serviceTime;
        
        CountDownLatch countDownLatch;

        public Worker(String name, int serviceTime, CountDownLatch countDownLatch) {
            this.name = name;
            this.serviceTime = serviceTime;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            System.out.println(name+"开始送餐,当前时间:"+sdf.format(new Date()));
            try {
				Thread.sleep(serviceTime);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
            System.out.println(name+"结束送餐,当前时间:"+sdf.format(new Date()));
            countDownLatch.countDown();
        }
    }
外面小哥李四开始送餐,当前时间:2021-12-22 11:31:16
外面小哥张三开始送餐,当前时间:2021-12-22 11:31:16
外面小哥李四结束送餐,当前时间:2021-12-22 11:31:26
外面小哥张三结束送餐,当前时间:2021-12-22 11:31:26
外卖单子全部送完,结束时间:10014

原本需要 20 秒送餐时间,两个人 10 秒就完成了,效率真是大大的提高了。

什么是CyclicBarrier

CyclicBarrier的 arrier 英文是屏障,障碍,栅栏的意思。cyclic 是循环的意思,字面意思是可循环使用(Cyclic)的屏障(Barrier)。他要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有屏障拦截的线程才会继续运行。

CyclicBarrier 方法列表

CyclicBarrier 类有两个常用的构造方法:

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

这里的 parties 指的是需要几个线程一起到达,例如,初始化时 parties 里的计数是 3,于是拥有该 CyclicBarrier 对象的线程当达到 3 时就唤醒

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

这里的parties与上一个构造方法的解释是一样的,这里需要解释的是第二个入参(Runnable barrierAction),用于在所有线程达到屏障时,优先执行 barrierAction。

这里我们模拟一下,现在有一组运动员100米决赛,现在要等待所有运动员准备完成后,等待裁判吹口哨之后才能开跑。

public static void main(String[] args) {
		 CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
			    @Override
			    public void run() {
			        try {
			            System.out.println("=========等待裁判吹口哨=============");
			            //这里停顿两秒更便于观察线程执行的先后顺序
			            Thread.sleep(2000);
			            System.out.println("=========裁判吹口哨=============");
			        } catch (InterruptedException e) {
			            e.printStackTrace();
			        }
			    }
			});
	        Runner runner1 = new Runner(cyclicBarrier, "飞人1");
	        Runner runner2 = new Runner(cyclicBarrier, "飞人2");
	        Runner runner3 = new Runner(cyclicBarrier, "飞人3");

	        ExecutorService executorService = Executors.newFixedThreadPool(3);
	        
	        executorService.execute(runner1);
	        
	        executorService.execute(runner2);
	        
	        executorService.execute(runner3);

	        executorService.shutdown();

	    }

	}


	class Runner implements Runnable{

	    private CyclicBarrier cyclicBarrier;
	    private String name;

	    public Runner(CyclicBarrier barrier, String name) {
	        this.cyclicBarrier = barrier;
	        this.name = name;
	    }

	    @Override
	    public void run() {
	        try {
	            Thread.sleep(new Random().nextInt(5000));
	            System.out.println(name + ":准备开跑");
	            cyclicBarrier.await();
	            System.out.println(name + ":开跑");
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } catch (BrokenBarrierException e){
	            e.printStackTrace();
	        }
	    }

上面代码定义 Runner 类代表运动员,内部维护了一个共有的 CyclicBarrier,每个人都有准备的时间,准备完成后会调用 await 方法等待其他运动员,当所有运动员都准备完成,等裁判吹口哨就可以开跑了。

测试运行的结果

飞人3:准备开跑
飞人2:准备开跑
飞人1:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人1:开跑
飞人3:开跑
飞人2:开跑

那上面提到的循环利用又是怎么实现的呢,我们把上面的代码修改一下:

把屏障值改为 2
在这里插入图片描述
增加一个“飞人4” 参与赛跑
在这里插入图片描述

飞人3:准备开跑
飞人2:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人2:开跑
飞人3:开跑
飞人4:准备开跑
飞人1:准备开跑
=========等待裁判吹口哨=============
=========裁判吹口哨=============
飞人1:开跑
飞人4:开跑

最终结果是第一次先跑两个人,然后第二次再跑两个人。也就实现了屏障的循环使用。

什么是Semaphore

Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

比如:学生去食堂打饭,假如有 3 个窗口可以打饭,同一时刻也只能有 3 名学生打饭。第 4 个学生来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了 。

Semaphore 方法列表

acquire(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。

release(int permits)

释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少

availablePermits()

返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。

reducePermits(int reduction)

根据指定的缩减量减小可用许可的数目。

hasQueuedThreads()

查询是否有线程正在等待获取资源。

getQueueLength()

返回正在等待获取的线程的估计数目。该值仅是估计的数字。

tryAcquire(int permits, long timeout, TimeUnit unit)

如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。

acquireUninterruptibly(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。

更多的请查看 类信号量

这里我们模拟一下上面学生去食堂吃饭,3个窗口有学生在打饭,剩下的学生等着前面打完饭的学生,才能接下来的操作。

	 private static int count = 10;

	    public static void main(String[] args) {

	        ExecutorService executorService = Executors.newFixedThreadPool(count);

	        //指定最多只能有五个线程同时执行
	        Semaphore semaphore = new Semaphore(3);

	        Random random = new Random();
	        for (int i = 0; i < count; i++) {
	            final int no = i;
	            executorService.execute(new Runnable() {
	  
	                @Override
	                public void run() {
	                    try {
	                        //获得许可
	                        semaphore.acquire();
	                        System.out.println(no+"可以来打饭了");
	                        //模拟车辆通行耗时
	                        TimeUnit.SECONDS.sleep(3);
	                    
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }finally {
	                    	 System.out.println(no+"打好饭了,别的学生可以来打了");
	                    	    //释放许可
		                        semaphore.release();
	                    }
	                }
	            });
	        }

	        executorService.shutdown();

	    }
2可以来打饭了
0可以来打饭了
1可以来打饭了
2打好饭了,别的学生可以来打了
1打好饭了,别的学生可以来打了
0打好饭了,别的学生可以来打了
3可以来打饭了
5可以来打饭了
4可以来打饭了
5打好饭了,别的学生可以来打了
3打好饭了,别的学生可以来打了
4打好饭了,别的学生可以来打了
7可以来打饭了
6可以来打饭了
8可以来打饭了
6打好饭了,别的学生可以来打了
7打好饭了,别的学生可以来打了
8打好饭了,别的学生可以来打了
9可以来打饭了
9打好饭了,别的学生可以来打了

从上面的结果我们可以知道,同一时间只能有3个学生同时打饭,其他学生必须要等待前面的学生打好饭才可以继续打饭。

现在有个问题,所有等待的学生都想先拿到许可,先通行,怎么办?这就需要,用到锁了。就所有人都去抢,谁先抢到,谁就先走呗。

从 Semaphore的构造函数中就会发现,可以传入一个 boolean 值的参数,控制抢锁是否是公平的。

    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }

默认是非公平,可以传入 true 来使用公平锁。

总结

  • CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待
  • CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值
  • CountDownLatch 是一次性的, CyclicBarrier 可以循环利用
  • CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作
  • Semaphore ,需要拿到许可才能执行,并可以选择公平和非公平模式
    • 0
      点赞
    • 4
      收藏
      觉得还不错? 一键收藏
    • 0
      评论

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值