Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

概述

在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法。

CountDownLatch

上图TA刚开始被阻塞,三个线程T1,T2,T3每次调用countDown()方法cnt就减1,等到cnt=0时,TA才开始执行。

正如Java文档所描述的那样,CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch是在Java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。

CountDownLatch工作原理相对简单,可以简单看成一个倒计数器,在构造方法中指定初始值,每次调用countDown()方法时将计数器减1,而await()会等待计数器变为0。

主要接口分析

构造器

  1. CountDownLatch(int count)构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次, countDown方法会对count减一,直到count减到0的时候,当前调用await方法的线程继续执行。

主要方法

  1. countDown() 如果当前计数器的值大于1,则将其减1;若当前值为1,则将其置为0并唤醒所有通过await等待的线程;若当前值为0,则什么也不做直接返回。
  2. await() 等待计数器的值为0,若计数器的值为0则该方法返回;若等待期间该线程被中断,则抛出InterruptedException并清除该线程的中断状态。
  3. await(long timeout, TimeUnit unit) 在指定的时间内等待计数器的值为0,若在指定时间内计数器的值变为0,则该方法返回true;若指定时间内计数器的值仍未变为0,则返回false;若指定时间内计数器的值变为0之前当前线程被中断,则抛出InterruptedException并清除该线程的中断状态。
  4. getCount() 读取当前计数器的值,一般用于调试或者测试。

代码

召唤神龙需要集齐7颗龙珠才行。。

private static final int THREAD_COUNT_NUM = 7;
private static CountDownLatch latch = new CountDownLatch(THREAD_COUNT_NUM);

public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < THREAD_COUNT_NUM; i++) {
        int index = i;
        new Thread(() -> {
            System.out.println("第" + index + "颗龙珠已收集!");
            // 每收集到一颗龙组合,需要等待的颗数就减1
            latch.countDown();
        }).start();
    }
    // 上述7个线程执行完毕之后,才执行await后面的代码
    latch.await();
    System.out.println("集齐七颗龙珠,召唤神龙!");
}

运行结果

CyclicBarrier

在上图中,T1、T2、T3每调用一次await,说明当前线程到达barrier,计数减1,并且开始阻塞当前子线程,开始等待其他线程到达barrier,如果全部到达(计数为0),TA线程开始执行;

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier),和CountDownLatch非常类似,他也可以实现线程间的计数等待,但他的功能要比CountDownLatch更加强大一些。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。当某个线程调用了await方法之后,就会进入等待状态,并将计数器-1,直到所有线程调用await方法使计数器为0,才可以继续执行,由于计数器可以重复使用,所以我们又叫他循环屏障。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

主要接口分析

构造器

  1. CyclicBarrier(int parties) parties表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。CyclicBarrier强调的是parties个线程,大家相互等待,只要有一个没完成,所有人都得等着。
  2. CyclicBarrier(int parties, Runnable barrierAction)参数barrierAction是当这些线程都达到barrier状态时会执行的内容

主要方法

  1. await() 第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;等待其它参与方的到来(调用await())。如果当前调用是最后一个调用,则唤醒所有其它的线程的等待并且如果在构造CyclicBarrier时指定了action,当前线程会去执行该action,然后该方法返回该线程调用await的次序(getParties()-1说明该线程是第一个调用await的,0说明该线程是最后一个执行await的),接着该线程继续执行await后的代码;如果该调用不是最后一个调用,则阻塞等待;如果等待过程中,当前线程被中断,则抛出InterruptedException;如果等待过程中,其它等待的线程被中断,或者其它线程等待超时,或者该barrier被reset,或者当前线程在执行barrier构造时注册的action时因为抛出异常而失败,则抛出BrokenBarrierException。
  2. await(long timeout, TimeUnit unit) 与await()唯一的不同点在于设置了等待超时时间,等待超时时会抛出TimeoutException
  3. reset() 该方法会将该barrier重置为它的初始状态,并使得所有对该barrier的await调用抛出BrokenBarrierException

代码

还是用上收集七颗龙珠的例子,不过为了后面演示方便,输出的时候加了当前线程

简单版

private static final int THREAD_COUNT_NUM = 7;

public static void main(String[] args) throws InterruptedException {
    // 屏障点
    CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
        @Override
        public void run() {
            System.out.println("集齐七颗龙珠,召唤神龙!");
        }
    });
    // 线程
    for (int i = 0; i < THREAD_COUNT_NUM; i++) {
        int index = i + 1;
        Thread.sleep(5000);
        new Thread(() -> {
            try {
                System.out.println("当前线程:" + Thread.currentThread().getName());
                System.out.println("第" + index + "颗龙珠已收集!");
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果

带参数

private static final int THREAD_COUNT_NUM = 7;

public static void main(String[] args) throws InterruptedException {
    // 屏障点
    CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
        @Override
        public void run() {
            System.out.println("集齐七颗龙珠,召唤神龙!");
        }
    });
    // 线程
    for (int i = 0; i < THREAD_COUNT_NUM; i++) {
        int index = i + 1;
        Thread.sleep(5000);
        new Thread(() -> {
            try {
                System.out.println("当前线程:" + Thread.currentThread().getName());
                System.out.println("第" + index + "颗龙珠已收集!");
                // 这里带了参数
                barrier.await(2000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果

到达barrier之后,如果已经超过等待时间,但是下一个还没有到barrier。就抛出异常,继续下一步操作

可重入代码

private static final int THREAD_COUNT_NUM = 7;

public static void main(String[] args) throws InterruptedException {
    // 屏障点
    CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
        @Override
        public void run() {
            System.out.println("集齐七颗龙珠,召唤神龙!");
        }
    });
    // 线程
    for (int i = 0; i < THREAD_COUNT_NUM; i++) {
        int index = i + 1;
        new Thread(() -> {
            try {
                System.out.println("当前线程:" + Thread.currentThread().getName());
                System.out.println("第" + index + "颗龙珠已收集!");
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }

    Thread.sleep(2000);

    // 重入
    for (int i = 0; i < THREAD_COUNT_NUM; i++) {
        int index = i + 1;
        new Thread(() -> {
            try {
                System.out.println("当前线程:" + Thread.currentThread().getName());
                System.out.println("第" + index + "颗龙珠已收集!");
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果

图片太长,只截取部分。。

CountDownLatch与CyclicBarrier的比较

CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:

  1. CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
  2. 调用CountDownLatch的countDown方法后,会阻塞主线程,当前子线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,不会阻塞主线程,会阻塞当前子线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
  3. CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;
  4. CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

Semaphore

信号量主要用于控制访问资源的线程个数,常常用于实现资源池,如数据库连接池,线程池...

在Semaphore中,acquire方法用于获取资源,有的话,继续执行(使用结束后,记得释放资源),没有资源的话将阻塞直到有其它线程调用release方法释放资源;

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

主要接口分析

构造器

  1. Semaphore(int permits)参数permits表示许可数目,即同时可以允许多少线程进行访问
  2. Semaphore(int permits, boolean fair)这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可

主要方法

  1. acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
  2. acquire(int permits)获取permits个许可
  3. release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
  4. release(int permits)释放permits个许可

这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

  1. tryAcquire()尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
  2. tryAcquire(long timeout, TimeUnit unit)尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
  3. tryAcquire(int permits)尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
  4. tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

另外还可以通过availablePermits()方法得到可用的许可数目。

代码

拿停车位举例,有10个司机想要停车,但是总共只有5个停车位。

private static final int DRIVER_COUNT = 10;
private static final int PARKING_SPOT_COUNT = 5;

private static ExecutorService threadPool = Executors.newFixedThreadPool(DRIVER_COUNT);

private static Semaphore semaphore = new Semaphore(PARKING_SPOT_COUNT);

public static void main(String[] args) {
    for (int i = 0; i < DRIVER_COUNT; i++) {
        threadPool.execute(new Driver(String.valueOf(i), semaphore));
    }
    threadPool.shutdown();
}

static class Driver implements Runnable {
    private String id;
    private Semaphore semaphore;

    public Driver(String id, Semaphore semaphore) {
        this.id = id;
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(this.id + "号司机正在使用车位");
            Thread.sleep(1000);
            semaphore.release();
            System.out.println(this.id + "号司机离开车位");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果

下面对上面说的三个辅助类进行一个总结:

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

参考

https://blog.csdn.net/itmyhome1990/article/details/74971388

https://cloud.tencent.com/developer/article/1181493

http://www.jasongj.com/java/thread_communication/

https://www.cnblogs.com/dolphin0520/p/3920397.html

https://www.cnblogs.com/chenpi/p/5614290.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值