CountDownLatch用法简介

CountDownLatch是Java并发编程的常用类,是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,在闭锁上等待的线程就会被唤醒。

1、原理介绍

① 构造器中传入的count值是计数器的初始值,是闭锁需要等待的线程数量,这个值只能被设置一次,而且不能重新设置。
② await()方法会是调用此方法的线程阻塞,直到计数器的值减到0才被唤醒。
③ countDown()方法会使计数器值 -1,可在子线程执行完任务调用此方法,并且要尽量放在finally中,避免死锁。

2、场景1:让主线程等待,在多个子线程完成后再执行

2.1、说明

该使用场景是让主线程等待,在多个子线程完成后再执行,如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后、需要进行结果合并;或者多个数据操作完成后,需要数据检查。

public class Test1 {
    public static void main(String[] args) throws Exception {
        CountDownLatch count = new CountDownLatch(5);//子线程数量
        for (int i = 1; i <= 5; i++) {
            final int index = i;
            new Thread(() -> {
                System.out.println("子线程"+ index +"执行……");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    count.countDown();//计数值-1
                }
            }).start();
        }
        count.await();//主线程阻塞等待
        System.out.println("所有子线程完成后,主线程执行!");
    }
}

在这里插入图片描述

2.2、示例

有这样一种业务场景:需要请求多个外部API去获取数据,且多个API之间并无数据递进关系,此时可以采用并发请求的方式提高效率。
如在办理车辆业务平台中需要请求不同的API获取车辆信息和违章信息,采用同步方式请求耗时如下:

@RequestMapping("/test1")
public String test1() throws Exception {
    String plateNumber = "粤XXXXXX";
    String plateType = "02";
    String engine = "000000";
    String vin = "000000";
    String idCardNo = "000000000000000000";

    long start = System.currentTimeMillis();
    //获取车辆信息
    JSONObject vehicleInfo = vehicleInfoService.getVehicleInfo(plateNumber, plateType, engine, idCardNo);
    System.out.println("vehicleInfo: " + vehicleInfo);
    //获取违章信息
    JSONObject illegalInfo = illegalInfoService.getIllegalInfo(plateNumber, plateType, engine, vin);
    System.out.println("illegalInfo: " + illegalInfo);
    long end = System.currentTimeMillis();
    String s = "总耗时:" + (end - start);
    System.out.println(s);
    return s;
}
总耗时:2461

改成用CountDownLatch实现并发请求结果如下:

@RequestMapping("/test1")
public String test1() throws Exception {
    String plateNumber = "粤XXXXXX";
    String plateType = "02";
    String engine = "000000";
    String vin = "000000";
    String idCardNo = "000000000000000000";

    CountDownLatch count = new CountDownLatch(2);//子线程数量
    long start = System.currentTimeMillis();
    //获取车辆信息
    new Thread(() -> {
        try {
            JSONObject vehicleInfo = vehicleInfoService.getVehicleInfo(plateNumber, plateType, engine, idCardNo);
            System.out.println("vehicleInfo: " + vehicleInfo);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            count.countDown();//计数值-1
        }
    }).start();

    //获取违章信息
    new Thread(() -> {
        try {
            JSONObject illegalInfo = illegalInfoService.getIllegalInfo(plateNumber, plateType, engine, vin);
            System.out.println("illegalInfo: " + illegalInfo);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            count.countDown();//计数值-1
        }
    }).start();

    count.await();//主线程阻塞等待
    long end = System.currentTimeMillis();
    String s = "总耗时:" + (end - start);
    System.out.println(s);
    return s;
}
总耗时:1012

3、场景2:让多个子线程等待,在主线程完成后再执行

3.1、说明

该场景是让多个子线程等待,在主线程完成后,用countdown()将计数器减为0时,同时开始执行,类似发令枪,让多个运动员同时起跑,常用于模拟高并发场景。

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(1);//主线程数量
        for (int i = 1; i <= 5; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    count.await();//调用此方法的子线程会被阻塞在这,直到主线程计数值为0被唤醒
                    System.out.println("子线程"+index+"开始执行……");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println("主线程开始执行……");
        TimeUnit.SECONDS.sleep(2);
        System.out.println("主线程执行完成,唤醒所有子线程!");
        count.countDown();//主线程计数值减到0,唤醒子线程
    }
}

在这里插入图片描述

3.2、示例

作高并发测试工具,以下以测试SimpleDateFormat类线程不安全场景为例:

@Service
public class SdfService {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public void test1() throws Exception{
        final String[] strArr = new String[] {"2021-02-09 10:24:00", "2021-02-10 20:48:00", "2021-02-11 12:24:00"};
        int max = 2;
        int min = 0;
        Random random = new Random();
        int s = random.nextInt(max)%(max-min+1) + min;
        System.out.println(Thread.currentThread().getName()+ "\t" + sdf.parse(strArr[s]));
    }
}

测试方法如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestCountDownLatch {

    @Autowired
    private SdfService sdfService;

    @Test
    public void test1() throws Exception{
        CountDownLatch count = new CountDownLatch(1);
        for (int i = 1; i <= 50; i++) {//50个线程并发
            new Thread(() -> {
                try {
                    count.await();
                    sdfService.test1();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
        TimeUnit.SECONDS.sleep(2);
        System.out.println("所有子线程同时开始解析字符日期:");
        count.countDown();
    }
}

在这里插入图片描述


版权声明:本文为weixin_44360895原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。