CountDownLatch使用踩坑

先说结论

使用CountDownLatch时,调用countDown()方法释放锁的逻辑必须写在fiinally代码块中。如下图所示。
在这里插入图片描述

背景

最近在使用CountDownLatch聚合统计数据时遇到一个很奇怪的问题,前端请求后端一个接口,接口一直没有响应,数据量也不是很大,正常情况下
1-2s后端一定会返回数据给前端了。这个现象在测试环境没看到,到了生产环境就出问题了,最后重新检查了代码,发现CountDownLatch使用上存在问题。

场景分析

需求是这样的:对两项数据做数据统计,最后汇总,返回给前端。于是想到了使用CountDownLatch,阻塞主线程,开启两个子线程分别去做数据查询,子线程将查询结果放入一个共享的Map中,两个子线程释放共享锁(CountDownLatch底层实现共享锁实现)后,唤醒主线程,主线程执行后续逻辑。

实际的业务场景,抽象成下面的demo实现

package com.company.review;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatch使用踩坑——多线程聚合统计指标
 * @Author: Alan
 * @Date: 2022/11/26 23:02
 */
@Slf4j
public class CountDownLatchDemo {

    //用于聚合所有的统计指标
    private static Map<String,Object> map = new ConcurrentHashMap();
    //创建计数器,这里需要统计2个指标
    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) {
        //记录开始时间
        long startTime = System.currentTimeMillis();

        Thread countUserThread = new Thread(() -> {
            try {
                log.info("正在统计新增用户数量");
                Thread.sleep(3000);//任务执行需要3秒
                map.put("userNumber", 100);//保存结果值
                log.info("统计新增用户数量完毕");
                countDownLatch.countDown();//标记已经完成一个任务
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread countOrderThread = new Thread(() -> {
            try {
                log.info("正在统计订单数量");
                log.info("模拟统计订单数量时抛出异常{}",1/0);
                map.put("countOrder", 20);//保存结果值
                log.info("统计订单数量完毕");
                countDownLatch.countDown();//标记已经完成一个任务
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        //启动子线程执行任务
        countUserThread.start();
        countOrderThread.start();

        try {
            log.info("---主线程被阻塞----");
            //主线程等待所有统计指标执行完毕
            countDownLatch.await();
            log.info("---主线程被唤醒----");
            long endTime = System.currentTimeMillis();//记录结束时间
            log.info("------统计指标全部完成--------");
            log.info("统计结果为:" + map);
            log.info("任务总执行时间为" + (endTime - startTime) + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果输出:
在这里插入图片描述
如上面的输出可以看出,线程0正常执行了,线程1抛出了一个异常,主线程被阻塞后就再没有被唤醒(这也就是背景中说的,前端一直没有接收到响应结果的原因,说白了就是主线程一直处于阻塞状态,没有往后执行)。

到底是什么原因导致主线程一直处于阻塞状态呢?这就得盘盘countdownLatch的底层实现了。CountDownLatch是AQS包下的一个工具类,其本质是一个共享锁,我们创建CountDownLatch时在构造方法中传入的数字即是共享锁的个数,每次调用countDown()方法时,会释放一个共享锁,直到所有锁释放时,会调用unpark方法唤醒正在同步队列中阻塞的线程。也就是说,如果共享锁一直没有释放完,那么就永远都不会调用unpark()方法唤醒同步队列中阻塞的线程。

回到上面的例子,线程0执行完业务逻辑,调用countDown()方法正常释放锁,线程1执行业务逻辑发生异常,无法调用countDown()方法正常释放锁,所以处于同步队列中的线程主线程一直没有被唤醒

解决方式

解决方案其实很简单,正如上面分析的,线程1是因为发生了异常就没有执行countDown()方法释放锁,如果不发生异常,其实它还是会释放锁的,所以我们将释放锁的逻辑放到一个finally代码块即可,这样无论如何,锁都会被释放,抛出的异常也可以被后续的程序所感知到。这样就不至于像我在背景介绍中所说的那样,前端一直无法获取到后端结果,后端一直待响应状态。

正确的使用姿势

package com.company.review;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatch使用踩坑——多线程聚合统计指标
 * @Author: Alan
 * @Date: 2022/11/26 23:02
 */
@Slf4j
public class CountDownLatchDemo {

    //用于聚合所有的统计指标
    private static Map<String,Object> map = new ConcurrentHashMap();
    //创建计数器,这里需要统计2个指标
    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) {
        //记录开始时间
        long startTime = System.currentTimeMillis();

        Thread countUserThread = new Thread(() -> {
            try {
                log.info("正在统计新增用户数量");
                Thread.sleep(3000);//任务执行需要3秒
                map.put("userNumber", 100);//保存结果值
                log.info("统计新增用户数量完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();//标记已经完成一个任务
            }
        });
        Thread countOrderThread = new Thread(() -> {
            try {
                log.info("正在统计订单数量");
                log.info("模拟统计订单数量时抛出异常{}",1/0);
                map.put("countOrder", 20);//保存结果值
                log.info("统计订单数量完毕");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();//标记已经完成一个任务
            }
        });
        //启动子线程执行任务
        countUserThread.start();
        countOrderThread.start();

        try {
            log.info("---主线程被阻塞----");
            //主线程等待所有统计指标执行完毕
            countDownLatch.await();
            log.info("---主线程被唤醒----");
            long endTime = System.currentTimeMillis();//记录结束时间
            log.info("------统计指标全部完成--------");
            log.info("统计结果为:" + map);
            log.info("任务总执行时间为" + (endTime - startTime) + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

注意看每个线程都是在finally代码块中对countDown()方法进行的调用,保证程序一定会执行释放锁的逻辑
在这里插入图片描述

CountDownLatch是一个多线程编程工具,它典型的用法是某一线程在开始运行前等待n个线程执行完毕。在使用CountDownLatch时,首先需要将计数器初始化为n,然后当一个任务线程执行完毕时,就将计数器减1。当计数器的值变为0时,通过await()方法等待的线程就会被唤醒,继续执行后续的操作。这个工具常用于启动一个服务时,主线程需要等待多个组件加载完毕之后再继续执行。注意,CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后无法再次对其设置值,也无法再次使用CountDownLatch。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [CountDownLatch的理解和使用](https://blog.csdn.net/weixin_30951231/article/details/98973834)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [CountDownLatch用法详解](https://blog.csdn.net/qq812908087/article/details/81112188)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值