CountDownLatch、CyclicBarrier & Semaphore

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012152619/article/details/73865001

CountDownLatch

CountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。有时候会有这样的需求,多个线程同时工作,然后其中几个可以随意并发执行,但有一个线程需要等其他线程工作结束后,才能开始。举个例子,开启多个线程分块下载一个大文件,每个线程只下载固定的一截,最后由另外一个线程来拼接所有的分段,那么这时候我们可以考虑使用CountDownLatch来控制并发。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。


上图中,TA线程调用了CountDownLatch的wati()方法,进入阻塞状态,等待计数器归零;线程T1、T2、T3在合适的场景下分别调用了CountDownLatch的countDown()方法,使计数器减1;当计数器归零后,TA线程解除阻塞,继续运行。

比如,模拟一个炒菜的场景(半夜,我真的有点饿了……),炒一份番茄鸡蛋,大酒店都是分工合作,流水线作业,假设需要四个人完成,三个配菜工,一个准备番茄,一个准备鸡蛋,一个准备调味品,还有一个主厨负责掌勺。巧妇难为无米炊,显然,主厨必须等配菜工把材料备好后才能动手。

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CountDownLatchTest {
 
    static class AssistantCook implements Runnable {
        private CountDownLatch countDownLatch;
        private String name;
 
        public AssistantCook(CountDownLatch countDownLatch, String name) {
            this.countDownLatch =countDownLatch;
            this.name = name;
        }
 
        @Override
        public void run() {
            try {
                Random random = new Random();
                Thread.sleep(random.nextInt(1000));// 模拟工作耗时
                System.out.println(name +"工作完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown(); //countDownLatch减1
            }
        }
    }
 
    static class ChiefCook implements Runnable{
        private CountDownLatch countDownLatch;
        private String name;
 
        public ChiefCook(CountDownLatch countDownLatch, String name) {
            this.countDownLatch = countDownLatch;
            this.name = name;
        }
 
        @Override
        public void run() {
            try {
                System.out.println(name +"等待中……");
                countDownLatch.await(); // 等待countDownLatch归零
                System.out.println(name +"开始炒菜");
                Random random = new Random();
               Thread.sleep(random.nextInt(1000)); // 模拟工作耗时
                System.out.println(name +"已将菜炒好!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static void main(String[] args) {
 
        CountDownLatch countDownLatch = new CountDownLatch(3); // 计数器初始值为3
 
        ChiefCook chiefCook = new ChiefCook(countDownLatch, "主厨");
        AssistantCook tomatoCook = new AssistantCook(countDownLatch, "西红柿配菜工");
        AssistantCook eggCook = new AssistantCook(countDownLatch, "西红柿配菜工");
        AssistantCook spicesCook = new AssistantCook(countDownLatch, "调料配菜工");
        
        ExecutorService pool =Executors.newFixedThreadPool(4);
        pool.submit(chiefCook);
        pool.submit(tomatoCook);
        pool.submit(eggCook);
        pool.submit(spicesCook);

        pool.shutdown();
    }
}

打印结果如下:

主厨等待中……
西红柿配菜工工作完毕
调料配菜工工作完毕
西红柿配菜工工作完毕
主厨开始炒菜
主厨已将菜炒好!

CountDownLatch的作用与join方法很像,都可以实现某个线程等待其他线程完成后再进行后续操作的场景。但是,CountDownLatch更加灵活,适用范围更广。比如,某个线程等待其他线程完成部分工作时即可继续运行,join方法就无法胜任,而CountDownLatch通过计数器提供了更灵活的控制。


CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。await() 方法没被调用一次,计数便会减少1,并阻塞住当前线程。当计数减至0时,阻塞解除,所有在此 CyclicBarrier 上面阻塞的线程开始运行。在这之后,如果再次调用 await() 方法,计数就又会变成 N-1,新一轮重新开始,这便是 Cyclic 的含义所在。

CyclicBarrier.await()方法带有返回值,用来表示当前线程是第几个到达这个 Barrier 的线程。和 CountDownLatch 一样,CyclicBarrier 同样可以可以在构造函数中设定总计数值。与 CountDownLatch 不同的是,CyclicBarrier 的构造函数还可以接受一个 Runnable,会在 CyclicBarrier 被释放时执行。

下面我们来模拟一个机场大巴发车的场景,每辆大巴载客10人,滚动发车,人满即走,每次发车后需要将本次发车的时间、乘客信息登记下来。

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CyclicBarrierTest {
 
    static int busCounter = 0;
 
    public static synchronized void departBus(){
        busCounter++;
    }
 
    private static String getTime() {
        DateFormat df = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss");
        return df.format(new Date());
    }
 
    public static class Passenger implementsRunnable {
 
        public Passenger(CyclicBarrier barrier){
            this.barrier = barrier;
        }
 
        CyclicBarrier barrier;
 
        @Override
        public void run() {
            try {
                Random random = new Random();
               Thread.sleep(random.nextInt(10000)); // 到达时间随机
                String arriveTime = getTime();
                int index = barrier.await();
                System.out.println("第" + (10 - index) + "位乘客于" +arriveTime + "到达");
            } catch (BrokenBarrierException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static class BusManager implements Runnable {
 
        @Override
        public void run() {
            departBus();
           System.out.println("**********************");
            System.out.println("第" + busCounter + "辆大巴于" +getTime() + "发车");
           System.out.println("乘客到达时间如下:");
        }
    }
 
    public static void main(String[] args) {
 
        CyclicBarrier barrier = new CyclicBarrier(10, new BusManager());
 
        ExecutorService pool =Executors.newFixedThreadPool(10);
        for (int i = 0; i < 50; i++) {  //共50名乘客
            pool.submit(new Passenger(barrier));
        }
        pool.shutdown();
    }
 
}

打印结果如下:

**********************
第1辆大巴于2017-06-27 12:13:08发车
乘客到达时间如下:
第10位乘客于2017-06-27 12:13:08到达
第2位乘客于2017-06-27 12:13:00到达
第4位乘客于2017-06-27 12:13:03到达
第1位乘客于2017-06-27 12:12:58到达
第6位乘客于2017-06-27 12:13:04到达
第8位乘客于2017-06-27 12:13:07到达
第7位乘客于2017-06-27 12:13:06到达
第5位乘客于2017-06-27 12:13:03到达
第3位乘客于2017-06-27 12:13:02到达
第9位乘客于2017-06-27 12:13:07到达
**********************
第2辆大巴于2017-06-27 12:13:17发车
乘客到达时间如下:
第10位乘客于2017-06-27 12:13:17到达
第2位乘客于2017-06-27 12:13:10到达
第4位乘客于2017-06-27 12:13:11到达
第6位乘客于2017-06-27 12:13:14到达
第8位乘客于2017-06-27 12:13:15到达
第1位乘客于2017-06-27 12:13:08到达
第9位乘客于2017-06-27 12:13:16到达
第7位乘客于2017-06-27 12:13:14到达
第5位乘客于2017-06-27 12:13:13到达
第3位乘客于2017-06-27 12:13:11到达
**********************
第3辆大巴于2017-06-27 12:13:27发车
乘客到达时间如下:
第10位乘客于2017-06-27 12:13:27到达
第2位乘客于2017-06-27 12:13:20到达
第4位乘客于2017-06-27 12:13:21到达
第6位乘客于2017-06-27 12:13:24到达
第7位乘客于2017-06-27 12:13:25到达
第8位乘客于2017-06-27 12:13:25到达
第1位乘客于2017-06-27 12:13:18到达
第9位乘客于2017-06-27 12:13:25到达
第5位乘客于2017-06-27 12:13:22到达
第3位乘客于2017-06-27 12:13:21到达
**********************
第4辆大巴于2017-06-27 12:13:36发车
乘客到达时间如下:
第10位乘客于2017-06-27 12:13:36到达
第2位乘客于2017-06-27 12:13:28到达
第4位乘客于2017-06-27 12:13:29到达
第3位乘客于2017-06-27 12:13:29到达
第7位乘客于2017-06-27 12:13:32到达
第1位乘客于2017-06-27 12:13:28到达
第9位乘客于2017-06-27 12:13:36到达
第8位乘客于2017-06-27 12:13:35到达
第6位乘客于2017-06-27 12:13:30到达
第5位乘客于2017-06-27 12:13:29到达
**********************
第5辆大巴于2017-06-27 12:13:46发车
乘客到达时间如下:
第10位乘客于2017-06-27 12:13:46到达
第2位乘客于2017-06-27 12:13:37到达
第1位乘客于2017-06-27 12:13:37到达
第4位乘客于2017-06-27 12:13:40到达
第5位乘客于2017-06-27 12:13:40到达
第6位乘客于2017-06-27 12:13:40到达
第7位乘客于2017-06-27 12:13:41到达
第8位乘客于2017-06-27 12:13:43到达
第3位乘客于2017-06-27 12:13:39到达
第9位乘客于2017-06-27 12:13:45到达

在很多场景下,CountDownLatch与 CyclicBarrier可以互换,都能实现类似的功能,细微的差别在于:

l   CountDownLatch 适用于一个主线程等待一组工作线程的任务完毕才能继续执行的场景;

l   CyclicBarrier 适用于一组线程在一个时间点上达成一致才开始工作的场景


Semaphore

Semaphore是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的事情后归还,超过阈值后,线程申请许可信号将会被阻塞。

可以把Semaphore比作控制流量的红绿灯,比如某条马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入马路,但是如果前一百辆中有五辆车已经驶离马路,那么后面就允许有5辆车驶入马路。这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接池。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控。

Semaphore可以看做是拥有多把锁的同步机制,而synchronized关键字是只有一把锁(互斥锁)的同步机制。

互斥锁(mutex)是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个。一般的用法是用于串行化对关键代码的访问,保证这段代码不会被并行的运行。

Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。当N=1的情况,称为二元信号量(binary semaphore),其功能类似于互斥锁。可见,互斥锁相当于一种特殊的Semaphore。

下面使用Semaphore实现这样一个场景:某个美食店共有共有20个座位,在用餐高峰期肯定应对不了蜂拥而至的吃货,后到的吃货只能去拿号排队,等有人吃完离场了才能进去。

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
 
public class SemaphoreTest {
 
    static class Eater implements Runnable {
 
        int index;
        Semaphore semaphore;
 
        public Eater(int index, Semaphore semaphore) {
            this.index = index;
            this.semaphore = semaphore;
        }
 
        @Override
        public void run() {
            try {
                int waiterCount =semaphore.getQueueLength();
                System.out.println("第" + index + "个客人到达,当前等待者共有" +waiterCount + "人");
                semaphore.acquire(); // 获取许可,有可能被阻塞
                System.out.println("第" + index + "个客人开始吃饭");
                Random random = new Random();
               Thread.sleep(random.nextInt(10000));
                semaphore.release(); // 释放许可
                System.out.println("第" + index + "个客人吃完离场");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
    }
 
    public static void main(String[] args) {
        // 构造函数中的第二个参数代表,是否为公平模式
        // 如果是,等待时间越长的线程优先获得许可
        // 如果否,会从等待的线程中随机选择,发放许可
        Semaphore semaphore = new Semaphore(20,true);
 
        ExecutorService pool =Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            pool.submit(new Eater(i + 1,semaphore));
        }
        pool.shutdown();
    }
}

打印结果如下:

第1个客人到达,当前等待者共有0人
第1个客人开始吃饭
第4个客人到达,当前等待者共有0人
第4个客人开始吃饭
第3个客人到达,当前等待者共有0人
第3个客人开始吃饭
第2个客人到达,当前等待者共有0人
第2个客人开始吃饭
第7个客人到达,当前等待者共有0人
第8个客人到达,当前等待者共有0人
第8个客人开始吃饭
第6个客人到达,当前等待者共有0人
第9个客人到达,当前等待者共有0人
第9个客人开始吃饭
第10个客人到达,当前等待者共有0人
第10个客人开始吃饭
第11个客人到达,当前等待者共有0人
第11个客人开始吃饭
第12个客人到达,当前等待者共有0人
第12个客人开始吃饭
第5个客人到达,当前等待者共有0人
第5个客人开始吃饭
第13个客人到达,当前等待者共有0人
第13个客人开始吃饭
第6个客人开始吃饭
第7个客人开始吃饭
第15个客人到达,当前等待者共有0人
第14个客人到达,当前等待者共有0人
第14个客人开始吃饭
第15个客人开始吃饭
第17个客人到达,当前等待者共有0人
第16个客人到达,当前等待者共有0人
第16个客人开始吃饭
第18个客人到达,当前等待者共有0人
第18个客人开始吃饭
第17个客人开始吃饭
第19个客人到达,当前等待者共有0人
第19个客人开始吃饭
第20个客人到达,当前等待者共有0人
第20个客人开始吃饭
第22个客人到达,当前等待者共有0人
第23个客人到达,当前等待者共有0人
第21个客人到达,当前等待者共有0人
第24个客人到达,当前等待者共有2人
第25个客人到达,当前等待者共有4人
第26个客人到达,当前等待者共有5人
第27个客人到达,当前等待者共有6人
第28个客人到达,当前等待者共有7人
第29个客人到达,当前等待者共有8人
第30个客人到达,当前等待者共有9人
第31个客人到达,当前等待者共有10人
第32个客人到达,当前等待者共有11人
第33个客人到达,当前等待者共有12人
……


猜你喜欢

转载自blog.csdn.net/u012152619/article/details/73865001