并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用

前言

JDK1.5提供了java.util.concurrent包(J.U.C)来简化并发编程,降低程序员使用并发编程的难度。JUC包中提供了几个并发控制工具,其中就有三巨头CountDownLatch、CyclicBarrier、Semaphore。本篇主要讲解三个工具的使用,其实现原理AbstractQueuedSynchronizer之后再详细讲解。

CountDownLatch

CountDownLatch(计数器闭锁)是同步辅助类,其功能主要是一个线程或多个线程等待其它线程操作完成后,再继续执行,CountDownLatch通过一个给定的计数器初始化,计数器的操作是原子操作,同时只能有一个线程操作该计数器。具体操作看下实例

public class CountDownLatchDemo {

    private static final int THREAD_COUNT = 10;

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                try {
                    // 模拟子线程耗时任务
                    Thread.sleep(500);
                    System.out.println("线程【" + Thread.currentThread().getName() + "】执行完成");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 计数器 -1
                    latch.countDown();
                }
            });
        }

        // 阻塞当前线程,直到所有子线程执行完成
        latch.await();
        System.out.println("所有子任务执行完成,线程【" + Thread.currentThread().getName() + "】继续执行");
        // 关闭线程池
        executor.shutdown();
    }
}

执行结果

线程【pool-1-thread-3】执行完成
线程【pool-1-thread-6】执行完成
线程【pool-1-thread-1】执行完成
线程【pool-1-thread-2】执行完成
线程【pool-1-thread-4】执行完成
线程【pool-1-thread-5】执行完成
线程【pool-1-thread-10】执行完成
线程【pool-1-thread-9】执行完成
线程【pool-1-thread-8】执行完成
线程【pool-1-thread-7】执行完成
所有子任务执行完成,线程【main】继续执行

CountDownLatch计数器初始值是10,主线程调用CountDownLatch#await()后被阻塞,直到所有子线程分别调用CountDownLatch#countDown()方法将计数器减一,直到计数器为0时,主线程才继续执行。
除此之外,CountDownLatch还提供了await()的重载方法
public boolean await(long timeout, TimeUnit unit)
用来指定线程阻塞时间,超时之后线程也会继续执行,即使子线程任务没有完成。

public class CountDownLatchDemo {

    private static final int THREAD_COUNT = 10;

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                try {
                    // 模拟子线程耗时任务
                    Thread.sleep(1000);
                    System.out.println("线程【" + Thread.currentThread().getName() + "】执行完成");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 计数器 -1
                    latch.countDown();
                }
            });
        }
        // 阻塞当前线程500毫秒
        latch.await(500, TimeUnit.MILLISECONDS);
        System.out.println("已等待500毫秒,线程【" + Thread.currentThread().getName() + "】继续执行");

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

执行结果

已等待500毫秒,线程【main】继续执行
线程【pool-1-thread-2】执行完成
线程【pool-1-thread-1】执行完成
线程【pool-1-thread-3】执行完成
线程【pool-1-thread-4】执行完成
线程【pool-1-thread-5】执行完成
线程【pool-1-thread-6】执行完成
线程【pool-1-thread-7】执行完成
线程【pool-1-thread-9】执行完成
线程【pool-1-thread-8】执行完成
线程【pool-1-thread-10】执行完成

CountDownLatch计数器初始值是10,主线程调用CountDownLatch#(long timeout, TimeUnit unit)后被阻塞指定时间,超时后主线程继续执行,此时子线程的任务并没有完成。
CountDownLatch提供的方法比较少,用法也比较简单,但是功能却非常强大。

CyclicBarrier

CyclicBarrier(循环屏障)同步辅助类,其功能主要是多个线程相互等待,都到达同一个屏障点之后再继续执行。通过一个给定的计数器初始化,计数器的操作是原子操作,同时只能有一个线程操作该计数器。计数器执行的是+1操作,由于计数器释放等待的线程后可以重用,所以又称为循环屏障。具体操作如下,代码演示的是线程池执行9个task,但是CyclicBarrier的计数器最大值为3,所以task会被分为三次执行完成。

public class CyclicBarrierDemo {

    // 循环屏障计数值
    private static final int THREAD_COUNT = 3;
    // 子线程执行任务数量
    private static final int TASK_COUNT = 3 * THREAD_COUNT;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        // 循环屏障
        CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT);
        for (int i = 0; i < TASK_COUNT; i++) {
            final int current = i;
            // 使每次循环结果输出结果更清晰
            Thread.sleep(500);
            executor.submit(() -> {
                try {
                    // 模拟耗时操作
                    Thread.sleep(1000);
                    System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程到达屏障");
                    barrier.await();
                    System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程继续执行...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

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

执行结果

1570074694897 -> 执行【task-0】的线程到达屏障
1570074695403 -> 执行【task-1】的线程到达屏障
1570074695899 -> 执行【task-2】的线程到达屏障
1570074695899 -> 执行【task-2】的线程继续执行...
1570074695899 -> 执行【task-1】的线程继续执行...
1570074695899 -> 执行【task-0】的线程继续执行...
1570074696404 -> 执行【task-3】的线程到达屏障
1570074696905 -> 执行【task-4】的线程到达屏障
1570074697406 -> 执行【task-5】的线程到达屏障
1570074697406 -> 执行【task-5】的线程继续执行...
1570074697406 -> 执行【task-3】的线程继续执行...
1570074697406 -> 执行【task-4】的线程继续执行...
1570074697906 -> 执行【task-6】的线程到达屏障
1570074698407 -> 执行【task-7】的线程到达屏障
1570074698907 -> 执行【task-8】的线程到达屏障
1570074698907 -> 执行【task-8】的线程继续执行...
1570074698907 -> 执行【task-7】的线程继续执行...
1570074698907 -> 执行【task-6】的线程继续执行...

首先看前6行输出,执行前两个task的线程到达屏障点后均被阻塞,直到执行第三个task的线程也到达屏障点,此时到达屏障点的线程数为3,也就是CyclicBarrier初始化时传入的计数值。根据4-6行的输出可以看到,三个线程同时开始继续执行。此时再看看前文所描述的CyclicBarrier功能:多个线程相互等待,都到达同一个屏障点之后再继续执行
再对比1-6行和7-12行输出,可以看到,7-12行只是重复了1-6行的输出(13-18行也是),执行的task不同而已。由此可以看出CyclicBarrier是可以重复使用的,所以叫循环屏障。
CyclicBarrier还有一个重载的构造方法
public CyclicBarrier(int parties, Runnable barrierAction)
第二个参数是Runnable对象,所有的线程到达屏障点后,继续往下执行之前,会执行该Runnable。

public class CyclicBarrierDemo {

    // 循环屏障计数
    private static final int THREAD_COUNT = 3;
    // 子线程执行任务数量
    private static final int TASK_COUNT = 2 * THREAD_COUNT;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();

        // 循环屏障,两个参数的构造方法
        CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
            System.out.println("Runnable barrierAction");
        });

        for (int i = 0; i < TASK_COUNT; i++) {
            final int current = i;
            Thread.sleep(500);
            executor.submit(() -> {
                try {
                    // 模拟耗时操作
                    Thread.sleep(1000);
                    System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程到达屏障");
                    barrier.await();
                    System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程继续执行...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

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

执行结果

1570087170174 -> 执行【task-0】的线程到达屏障
1570087170675 -> 执行【task-1】的线程到达屏障
1570087171176 -> 执行【task-2】的线程到达屏障
Runnable barrierAction
1570087171176 -> 执行【task-0】的线程继续执行...
1570087171176 -> 执行【task-1】的线程继续执行...
1570087171176 -> 执行【task-2】的线程继续执行...
1570087171676 -> 执行【task-3】的线程到达屏障
1570087172181 -> 执行【task-4】的线程到达屏障
1570087172683 -> 执行【task-5】的线程到达屏障
Runnable barrierAction
1570087172683 -> 执行【task-5】的线程继续执行...
1570087172683 -> 执行【task-4】的线程继续执行...
1570087172683 -> 执行【task-3】的线程继续执行...

执行task0、1、2的线程均到达屏障点时,会先执行构造方法里的Runnable,再继续执行task0、1、2。

CyclicBarrier和CountDownLatch的区别

CyclicBarrier作用和CountDownLatch非常相似,但是不完全一样,区别体现在两点:

  • CyclicBarrier可以循环使用,CountDownLatch不能
  • CyclicBarrier实现的是一组线程相互等待,都到达屏障点后再一起执行。CountDownLatch实现的是一个或多个线程等待其他线程执行完成某些任务,自己再继续执行

第二点不太好理解,所以我举个例子。
100米短跑赛场,8位运动员依次走入自己跑道起点,当最后1位走入跑道起点后,比赛自动开始(没有裁判发令枪),8位运动员同时冲向终点。这里描述的就是CyclicBarrier,8位运动员是相互等待的,不存在第1位走入了跑道起点就自己先跑了。只有8位运动员均走到了起点(8个线程均到达了屏障点),才能开始跑(线程继续执行)。
还是同样的情景,100米短跑赛场,裁判先走到起点处自己的位置,但是裁判必须等8位运动员都到达了起点,才能发令。这里描述的就是CountDownLatch,描述的是裁判必须等待8位运动员到达起点(一个线程必须等待其他线程执行完特点的操作),裁判才能发令(这个线程才能继续执行)。

这样对比着理解,才能更好的区分CountDownLatch和CyclicBarrier。

Semaphore

Semaphore(信号量)同步辅助类,这个信号量就相当于十字路口的红绿灯,如果十字路口人烟稀少,实际上不怎么需要红绿灯,只有车流量很大的时候才能让红绿灯大显神威。所以Semaphore就是用来控制某个资源并发访问的线程个数,常用于某些仅能提供有限服务的资源,如数据库连接数。下面是Semaphore简单使用方式

public class SemaphoreDemo {

    // 允许的最大并发数
    private static final int THREAD_COUNT = 3;
    // 子线程执行任务数量
    private static final int TASK_COUNT = 3 * THREAD_COUNT;

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(THREAD_COUNT);

        for (int i = 0; i < TASK_COUNT; i++) {
            final int current = i;
            executor.submit(() -> {
                try {
                    // 请求许可
                    semaphore.acquire();
                    Thread.sleep(1000);
                    System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程已获取许可,继续执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放许可
                    semaphore.release();
                }
            });
        }

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

执行结果

1570089725945 -> 执行【task-1】的线程已获取许可,继续执行
1570089725946 -> 执行【task-2】的线程已获取许可,继续执行
1570089725946 -> 执行【task-0】的线程已获取许可,继续执行
1570089726951 -> 执行【task-3】的线程已获取许可,继续执行
1570089726954 -> 执行【task-4】的线程已获取许可,继续执行
1570089726955 -> 执行【task-5】的线程已获取许可,继续执行
1570089727952 -> 执行【task-6】的线程已获取许可,继续执行
1570089727955 -> 执行【task-7】的线程已获取许可,继续执行
1570089727957 -> 执行【task-8】的线程已获取许可,继续执行

输出结果分为三组1-3行、4-6行、7-9行,对比这三组可以发现,同一时刻,最多只有三个线程执行,也就是说Semaphore可以很容易的就做到了控制某一资源的最大并发数。

Semaphore#acquire()会阻塞等待获取许可的线程,当同时请求许可的线程过多时,可能会导致某些线程的等待时间过长,因此Semaphore提供了一系列尝试获取许可的方法tryAcquire(...),下面演示下这些方法

public class SemaphoreDemo {

    // 允许的最大并发数
    private static final int THREAD_COUNT = 3;
    // 子线程执行任务数量
    private static final int TASK_COUNT = 3 * THREAD_COUNT;

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(THREAD_COUNT);

        for (int i = 0; i < TASK_COUNT; i++) {
            final int current = i;
            executor.submit(() -> {
                try {
                    // 尝试获取许可
                    if (semaphore.tryAcquire(500, TimeUnit.MILLISECONDS)) {
                        System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程已获取许可,继续执行");
                        Thread.sleep(1000);
                        // 释放许可
                        semaphore.release();
                    } else {
                        System.out.println(System.currentTimeMillis() + " -> 执行【task-" + current + "】的线程超时,告辞...");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

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

执行结果

1570091102693 -> 执行【task-0】的线程已获取许可,继续执行
1570091102693 -> 执行【task-1】的线程已获取许可,继续执行
1570091102695 -> 执行【task-2】的线程已获取许可,继续执行
1570091103196 -> 执行【task-5】的线程超时,告辞...
1570091103196 -> 执行【task-6】的线程超时,告辞...
1570091103196 -> 执行【task-4】的线程超时,告辞...
1570091103196 -> 执行【task-3】的线程超时,告辞...
1570091103196 -> 执行【task-7】的线程超时,告辞...
1570091103198 -> 执行【task-8】的线程超时,告辞...

从输出结果可以看到,只有执行task-0、1、2的线程获取到了许可,其余的线程等待了500毫秒,还没有获取到许可后就放弃了。

总结

以上就是并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用方法,可以看到,三个工具使用起来都非常简单,但是完成的功能却异常强大,所以想学好并发编程,这三个工具必须非常熟悉,明白三者的区别,以及适用场景,才能写出更优雅的并发编程代码。

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/101979116
今日推荐