Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)

472 阅读5分钟

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列

上篇文章分析了Semaphore/CountDownLatch/CyclicBarrier 实现原理,这次从应用的角度探索三者适用场合及其使用方式。
通过本篇文章,你将了解到:

1、场景引入
2、Semaphore 适用场景及使用方式
3、CountDownLatch 适用场景及使用方式
4、CyclicBarrier 适用场景及使用方式
5、三者适用场景总结

1、场景引入

网上很多文章在讲解这几个类的时候,分别举例讲解不同场景下的应用,这些例子本身没有紧密关联,不好横向对比。
因此,此处我们先预设整体场景。

参与主体

5位顾客到餐馆吃火锅,餐馆里有3位服务员。
桌上最多只能容下4盘菜。

image.png

活动内容

1、服务员上菜,顾客吃菜,服务员回收空盘子。
2、服务员涮1盘牛肚,需要烫5秒后顾客才能吃。
3、顾客自己上菜,等到每个人都上了一份菜之后才开吃。

2、Semaphore 适用场景及使用方式

场景说明

服务员开始上菜,当上了4盘菜之后,就不能再上了。需要将菜倒进锅里,然后将空盘子回收后才可以继续上菜。
这其实是典型共享有限资源场景,此处的资源即是:桌子上能放的菜盘子数。

Semaphore 使用

用Semaphore 模拟这种场景:

public class TestThread {

    //最大许可数量
    Semaphore semaphore = new Semaphore(4);

    //竞争共享资源的线程数
    final int threadCount = 5;

    class SemaphoreThread extends Thread{
        private Semaphore semaphore;
        public SemaphoreThread(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 准备上菜 桌子上还可以放 " +
                        semaphore.availablePermits() + " 盘菜");
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " 已经上菜了");
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            finally {
                semaphore.release();
                System.out.println(Thread.currentThread().getName() + " 回收空盘子 桌子上还可以放 " +
                        semaphore.availablePermits() + " 盘菜");
            }
        }
    }

    private void testSemaphore() {
        for (int i = 0 ;i < threadCount; i++) {
            Thread thread = new SemaphoreThread(semaphore);
            thread.setName("服务员" + (i + 1));
            thread.start();
        }
    }

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testSemaphore();
    }
}

将每个服务员当作线程,将桌子上可放的盘子数量当作许可。
服务员上了菜之后,将空盘子回收。
打印如下:

image.png

这结果可能不是那么明显,是因为服务员太少了,上菜速度不够快,因此增加服务员数量,设置:

final int threadCount = 10;

再来看结果:

image.png

可以看出中途桌子上可放的盘子数量为0,说明有的服务员需要等待其他服务员回收空盘子。

Semaphore 许可的数量和线程数不存在必然的联系。

3、CountDownLatch 适用场景及使用方式

场景说明

服务员给顾客们涮牛肚,为了保证口味,该牛肚只需要烫5秒钟即可,在没烫好之前,顾客只能干巴巴等待,服务员烫好后会告知顾客可以吃了。

CountDownLatch 使用

Demo1

用CountDownLatch 模拟这种场景:

public class TestThread {
    //设置倒数计数
    CountDownLatch countDownLatch = new CountDownLatch(5);

    //等待计数的线程个数
    final int threadWaitCount = 5;
    //执行倒数计数的线程个数
    final int threadCLCount = 1;

    class WaitThread extends Thread{
        private CountDownLatch countDownLatch;
        public WaitThread(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 等着吃牛肚 当前倒数计数: " + countDownLatch.getCount());
                //等待倒数结束
                countDownLatch.await();
                System.out.println(Thread.currentThread().getName() + " 开始吃牛肚");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    class CountDownThread extends Thread {
        private CountDownLatch countDownLatch;
        public CountDownThread(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            while (countDownLatch.getCount() > 0) {
                //倒数计数
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + " 在倒数,当前计数:" + countDownLatch.getCount());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void testCountDownLatch() {
        for (int i = 0 ;i < threadWaitCount; i++) {
            Thread thread = new WaitThread(countDownLatch);
            thread.setName("顾客" + (i + 1));
            thread.start();
        }

        for (int i = 0; i < threadCLCount; i++) {
            Thread thread = new CountDownThread(countDownLatch);
            thread.setName("服务员" + (i + 1));
            thread.start();
        }
    }

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testCountDownLatch();
    }
}

结果如下:

image.png

注:现在只是一个服务员在倒数计数,可以多个服务员倒数计数。
可以看出:

CountDownLatch 倒数计数和线程数不存在必然的联系。

Demo2

Demo1 针对的是一个或者多个线程倒数计数,然而一些时候我们需要每个线程只计数一次。
如:每个服务员只负责上一个菜,顾客需要等每个服务员都上了菜之后,才开始吃。

public class TestThread {

    //等待计数的线程个数
    final int threadWaitCount = 2;
    //执行倒数计数的线程个数
    final int threadCLCount = 3;

    //设置倒数计数
    CountDownLatch countDownLatch = new CountDownLatch(threadCLCount);

    class WaitThread extends Thread{
        private CountDownLatch countDownLatch;
        public WaitThread(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 等待上菜 还差 " + countDownLatch.getCount() + " 个菜没上");
                //等待倒数结束
                countDownLatch.await();
                System.out.println(Thread.currentThread().getName() + " 菜上齐了,开吃");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    class CountDownThread extends Thread {
        private CountDownLatch countDownLatch;
        public CountDownThread(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 开始上菜");
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + " 完成上菜,还差 " + countDownLatch.getCount() + " 个菜没上");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void testCountDownLatch() {
        for (int i = 0 ;i < threadWaitCount; i++) {
            Thread thread = new WaitThread(countDownLatch);
            thread.setName("顾客" + (i + 1));
            thread.start();
        }

        for (int i = 0; i < threadCLCount; i++) {
            Thread thread = new CountDownThread(countDownLatch);
            thread.setName("服务员" + (i + 1));
            thread.start();
        }
    }

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testCountDownLatch();
    }
}

CountDownLatch 倒数计数设置为服务员(线程)人数,每个服务员上菜之后调用CountDownLatch.countDown()进行倒数计数。
结果如下:

image.png

4、CyclicBarrier 适用场景及使用方式

场景说明

还是以上菜为例,若是服务员太忙,那只能顾客自己上了,每位顾客上菜的时候看看菜齐了没(菜齐的标准是每位顾客都有一盘菜),若是发现菜上齐了就告知大伙儿可以开饭了。

CyclicBarrier 使用

用CyclicBarrier 模拟这种场景:

public class TestThread {

    //于栅栏处等待其它参与者
    final int threadWaitCount = 5;

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("最后一个菜上了,上齐了");
            nextGeneration();
        }
    };

    //初始值为线程个数
    CyclicBarrier cyclicBarrier = new CyclicBarrier(threadWaitCount, runnable);

    class WaitThread extends Thread{
        private CyclicBarrier cyclicBarrier;
        public WaitThread(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 准备上菜,当前上了 " + cyclicBarrier.getNumberWaiting()
                + " 个菜");
                try {
                    cyclicBarrier.await();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 菜上齐了,开吃");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void testCyclicBarrier() {
        for (int i = 0 ;i < threadWaitCount; i++) {
            Thread thread = new WaitThread(cyclicBarrier);
            thread.setName("顾客" + (i + 1));
            thread.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void nextGeneration() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("吃完了,开始下一轮上菜");
                testCyclicBarrier();
            }
        }).start();
    }

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testCyclicBarrier();
    }
}

结果如下:

image.png

可以看出:

1、CyclicBarrier 初始的参与者个数就是线程的个数。
2、CyclicBarrier 可以重复利用。

5、三者适用场景总结

image.png

下篇将总结线程并发涉及到的锁的知识点,也是对并发系列文章的一次简短回顾。

本文基于jdk1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java