关于CountDownLatch失效问题

       一、项目背景

         这几天要开发一个类似支付宝那种年度账单统计的功能,就是到元旦后支付完会把用户这一年的消费情况从各个维度(我们把这一个维度称作一个指标)统计分析形成一张报告展示给用户。

        这个功能实现用到了CountDownLatch。假如统计分析用户的年底消费账单是10个指标。则希望用10线程并发去分别统计这10个指标,等10个线程都完成计算后,最后在通过另外一个线程汇总10个指标返给前端展示给用户。

        二、问题描述

        其中出现了这样一个问题,生成第一个用户的年度账单是10个指标计算完后,最后一个线程进行最后的结果统计。这没问题。但是在生成第二个用户年底账单时,返给前端的是空。但是数据库里却生成了第二用户的年度账单。后面生成的所有用户年度账单都是空,且数据库都有每个用户的账单。

        三、错误代码示例

        

package com.lsl.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

@Controller
@RequestMapping("/latch")
public class CountDownLatchController {

    //创建固定线程池最大线程数10
    private static ExecutorService executor = Executors.newFixedThreadPool(10);

    //模拟并发任务数
    private static   int taskNum = 10;

    //计数器
    CountDownLatch latch = new CountDownLatch(taskNum);


    @PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String execTask(){

        for (int i = taskNum;i>=1;i--){
            String name = "thread";
            Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
//            try {
//                Map map = submit.get();
//                String ThreadName = map.get("name").toString();
//                String total = map.get("total").toString();
//                System.err.println("ThreadName:" + ThreadName + ",total=" + total);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            } catch (ExecutionException e) {
//                e.printStackTrace();
//            }
        }

        try {
            latch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //正常情况下,等10个任务执行完毕下面的主线程才输出
        System.out.println("主线程开始执行了.....");

        return "success";
    }



    /**
     * 线程任务
     */
    private  class CountNumTask implements Callable<Map>{
        private String name;
        private int num;
        private CountDownLatch latch;

        public CountNumTask(CountDownLatch latch,String name,int num){
            this.latch = latch;
            this.name = name;
            this.num = num;
        }

        @Override
        public Map call() throws Exception {
            long st = new Date().getTime();
            Map resultMap = new HashMap();
            String threadName = name + num;
            resultMap.put("name",threadName);
            int total = 0;
            for (int i =0;i<=num;i++){
                total += i;
            }
            Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
            resultMap.put("total",total);
            latch.countDown();
            long ed = new Date().getTime();
            System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
            return resultMap;
        }
    }
}

第一次调用截图:

第二次调用截图:

   

从上面截图可以看出,10个指标线程还没有运行完,主线程就先输出了。

四、原因分析

        原来是CountDownLatch latch = new CountDownLatch(taskNum);定义成成员变量了。这个应用定义成局部变量,也就是放在方法内。

        原因是spring托管的bean都是单例的,第一次调用结束后latch.getCount()已经是0了,然后后面的调用就不会等待前面子任务完成就开始执行主线程任务了。这就是为什么数据库里有每次的数据,而没有返给前端的原因。

        网上有的说法是错误:他们认为是线程内的latch.countDown();没有执行,应该把这个放在fianlly语句快内,保证改计数器减1操作每次都能执行。

        如果是这样那么计数器没有到0,如果在方法内latch.await(10, TimeUnit.SECONDS);这个语句就可以看出,10秒钟后主线程也会执行,那么上面的10个线程如果每个任务的耗时都超过10秒才能出现主线程比子任务输出早的情况。如果采用的是latch.await();那么主线程就会被永远阻塞了,因为计数器没有到0。这个前提是CountDownLatch latch = new CountDownLatch(taskNum)这个定义的是局部变量。

五、正确的代码示例

package com.lsl.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

@Controller
@RequestMapping("/latch")
public class CountDownLatchController {

    //创建固定线程池最大线程数10
    private static ExecutorService executor = Executors.newFixedThreadPool(10);

    //模拟并发任务数
    private static   int taskNum = 10;





    @PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String execTask(){
        //计数器
        CountDownLatch latch = new CountDownLatch(taskNum);
        for (int i = taskNum;i>=1;i--){
            String name = "thread";
            Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
//            try {
//                Map map = submit.get();
//                String ThreadName = map.get("name").toString();
//                String total = map.get("total").toString();
//                System.err.println("ThreadName:" + ThreadName + ",total=" + total);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            } catch (ExecutionException e) {
//                e.printStackTrace();
//            }
        }

        try {
            latch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //正常情况下,等10个任务执行完毕下面的主线程才输出
        System.out.println("主线程开始执行了.....");

        return "success";
    }



    /**
     * 线程任务
     */
    private  class CountNumTask implements Callable<Map>{
        private String name;
        private int num;
        private CountDownLatch latch;

        public CountNumTask(CountDownLatch latch,String name,int num){
            this.latch = latch;
            this.name = name;
            this.num = num;
        }

        @Override
        public Map call() throws Exception {
            long st = new Date().getTime();
            Map resultMap = new HashMap();
            String threadName = name + num;
            resultMap.put("name",threadName);
            int total = 0;
            for (int i =0;i<=num;i++){
                total += i;
            }
            Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
            resultMap.put("total",total);
//            if (num!=5)
            latch.countDown();
            long ed = new Date().getTime();
            System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
            return resultMap;
        }
    }
}

两次调用截图:

六、其他几点细节

        细节1、从代码分析,thread10是第一进入进入线程的,为什么确实最后进入线程的thread1先输出了呢?原因从截图中我打印的耗时就能看出来,就是thread10耗时最长,所以最晚输出。

        细节2、如果我把下图的代码放开,且把计数器还定义成成员变量,会有什么结果呢?(结果可能出乎大家意料很好玩哦

        

我把代码附下面:

package com.lsl.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;

@Controller
@RequestMapping("/latch")
public class CountDownLatchController {

    //创建固定线程池最大线程数10
    private static ExecutorService executor = Executors.newFixedThreadPool(10);

    //模拟并发任务数
    private static   int taskNum = 10;

    //计数器
    CountDownLatch latch = new CountDownLatch(taskNum);


    @PostMapping(value = "execTask", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String execTask(){

        for (int i = taskNum;i>=1;i--){
            String name = "thread";
            Future<Map> submit = executor.submit(new CountNumTask(latch,name, i));
            try {
                Map map = submit.get();
                String ThreadName = map.get("name").toString();
                String total = map.get("total").toString();
                System.err.println("ThreadName:" + ThreadName + ",total=" + total);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        try {
            latch.await(10, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //正常情况下,等10个任务执行完毕下面的主线程才输出
        System.out.println("主线程开始执行了.....");

        return "success";
    }



    /**
     * 线程任务
     */
    private  class CountNumTask implements Callable<Map>{
        private String name;
        private int num;
        private CountDownLatch latch;

        public CountNumTask(CountDownLatch latch,String name,int num){
            this.latch = latch;
            this.name = name;
            this.num = num;
        }

        @Override
        public Map call() throws Exception {
            long st = new Date().getTime();
            Map resultMap = new HashMap();
            String threadName = name + num;
            resultMap.put("name",threadName);
            int total = 0;
            for (int i =0;i<=num;i++){
                total += i;
            }
            Thread.sleep(num+100);//每个任务sleep不同,模拟线程耗时不一样
            resultMap.put("total",total);
//            if (num!=5)
            latch.countDown();
            long ed = new Date().getTime();
            System.err.println("ThreadName:" + threadName + ",total=" + total + ",耗时=" + (ed-st));
            return resultMap;
        }
    }
}

运行截图如下图:

        从上面截图是不是发现了很奇怪啊!计数器定义成了成员变量,第二次调用为什么主线程是等前面10子任务都完成了才输出呢?而且子任务的输出顺序也对了,是从thread10到thread1依次输出,虽然thread10耗时最长,也是第一个输出了!!!

        出现上述2个反常,大家知道什么原因吗?欢迎在评论区留言!!!

        具体原因我会过几天在公布吧!!!!

        【因为submit.get()方法会依次获取线程的结果。而不是先获取到最新执行完的线程结果

        细节3、如果把线程内的latch.countDown()位置调整到最开始位置,会出现什么结果呢?

        如下图:

        

运行结果截图如下:

从细节3的现象可以看出,latch.countDown()位置放到线程任务的最后面,这个很重要。因为在latch.wait()实时读取计数器的数值是否到0了,一旦到0了,后面的主线程就里面执行了。这就和另外的CyclicBarrier(循环栅栏)有所区别了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/911783.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【专题】2024年全球生物医药交易报告汇总PDF洞察(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p38191 在当今复杂多变的全球经济环境下&#xff0c;医药行业正面临着诸多挑战与机遇。2024 年&#xff0c;医药行业的发展态势备受关注。 一方面&#xff0c;全球生物医药交易活跃&#xff0c;2021 - 2023 年的交易中&#xff0c;已…

鸿蒙5.0时代:原生鸿蒙应用市场引领开发者服务新篇章

前言 10月22日原生鸿蒙之夜发布会宣布HarmonyOS NEXT正式发布&#xff0c;首个版本号&#xff1a;鸿蒙5.0。这次“纯血鸿蒙”脱离了底层安卓架构成为纯国产的独立系统&#xff0c;仅凭这一点就有很多想象空间。 目前鸿蒙生态设备已超10亿&#xff0c;原生鸿蒙操作系统在中国市…

3.PyCharm工具

第三方IDE&#xff0c;集成开发工具&#xff0c;官网下载。 社区版本&#xff0c;免费使用。 创建项目 配置解释器&#xff0c;创建python文件&#xff0c;编写代码&#xff0c;运行&#xff1a;

arkUI:Flex弹性布局的各个属性

arkUI&#xff1a;Flex弹性布局的简单使用 1 主要内容说明2 相关内容2.1 Flex弹性布局的方向2.1.1 源码1的简答说明2.1.2 源码1 &#xff08;Flex弹性布局的方向&#xff09;2.1.3 源码1运行效果2.1.3.1 当direction: FlexDirection.RowReverse2.1.3.2 当direction: FlexDirect…

串口接收,不定长数据接收

###1.CUBE-MX配置串口 2.我采用串口中断接收&#xff0c;打开中断接口 3.时钟同样8倍频&#xff0c;1分频&#xff0c;使用内部时钟 打开串口中断 main() { __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 启用空闲中断__HAL_UART_ENABLE_IT(&huart1, UART_IT_R…

TikTok本土店vs跨境店:解读TikTok小店差异

TikTok小店的两种主要的店铺类型&#xff1a;本土店和跨境店&#xff0c;虽然这两种店铺在功能上有相似之处&#xff0c;但它们在运营模式、市场定位、目标受众和面临的挑战等方面存在显著的区别。 一、定义与基本特征 1. TikTok本土店 本土店指的是在特定国家或地区内经营的…

深度学习——优化算法、激活函数、归一化、正则化

文章目录 &#x1f33a;深度学习面试八股汇总&#x1f33a;优化算法方法梯度下降 (Gradient Descent, GD)动量法 (Momentum)AdaGrad (Adaptive Gradient Algorithm)RMSProp (Root Mean Square Propagation)Adam (Adaptive Moment Estimation)AdamW 优化算法总结 经验和实践建议…

用 Python搭建一个微型的HTTP服务器用于传输 2024/11/9

使用内置的 http.server 模块,来搭建微型服务器。 快速启动服务器http.server --- HTTP 服务器Python 3.13.0 文档 声明:文章代码部分 由 ai 生成 创建一个简单的文件共享服务器 进入 需要共享的目录 再打开cmd 输入以下代码 python -m http.server 8000 打开服务器 设置主…

虚拟机linux7.9下安装mysql

1.MySQL官网下载安装包&#xff1a; MySQL :: Download MySQL Community Server https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.39-linux-glibc2.12-x86_64.tar.gz 2.解压文件&#xff1a; #tar xvzf mysql-5.7.39-linux-glibc2.12-x86_64.tar.gz 3.移动文件&#…

Turtlebot3 buger 硬件与操作平台详细介绍

引言 TurtleBot3 有三个版本&#xff0c;分别是紧凑型的 Burger、功能更强的 Waffle和性能提升的 Waffle Pi&#xff0c;分别适用于不同的应用需求。使用 Raspberry Pi 作为主控单板计算机&#xff08;SBC&#xff09;&#xff0c;而 Waffle Pi 可以使用更强大的 NVIDIA Jetson…

LabVIEW导入并显示CAD DXF文件图形 程序见附件

LabVIEW导入并显示CAD DXF文件图形 程序见附件 LabVIEW导入并显示CAD DXF文件图形 程序见附件 - 北京瀚文网星科技有限公司 LabVIEW广泛应用于自动化、数据采集、图形显示等领域。对于涉及CAD图形的应用&#xff0c;LabVIEW也提供了一些方法来导入和显示CAD DXF文件&#x…

北斗智能定位平板终端|三防平板|北斗有源终端|北斗搜救终端

在当今快速发展的科技时代&#xff0c;智能设备的应用已经渗透到我们生活的方方面面&#xff0c;从日常娱乐到专业工作&#xff0c;无一不彰显着科技的魅力。特别是在高精度定位领域&#xff0c;随着全球卫星导航系统&#xff08;GNSS&#xff09;技术的不断进步&#xff0c;智…

40.第二阶段x86游戏实战2-初识lua

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…

华为ENSP--ISIS路由协议

项目背景 为了确保资源共享、办公自动化和节省人力成本&#xff0c;公司E申请两条专线将深圳总部和广州、北京两家分公司网络连接起来。公司原来运行OSFP路由协议&#xff0c;现打算迁移到IS-IS路由协议&#xff0c;张同学正在该公司实习&#xff0c;为了提高实际工作的准确性和…

【Hadoop实训】Flume系统负载均衡测试

一、搭建并配置Flume机器 在master上&#xff0c;执行&#xff1a; scp -r /export/servers/flume slave1:/export/servers/scp -r /export/servers/flume slave2:/export/servers/scp /etc/profile slave1:/etc/profilescp /etc/profile slave2:/etc/profile 执行完上述指令后…

Java中的线程安全问题(如果想知道Java中有关线程安全问题的基本知识,那么只看这一篇就足够了!)

前言&#xff1a;多线程编程已经广泛开始使用&#xff0c;其可以充分利用系统资源来提升效率&#xff0c;但是线程安全问题也随之出现&#xff0c;它直接影响了程序的正确性和稳定性&#xff0c;需要对其进行深入的理解与解决。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解…

2024 CSS保姆级教程二 - BFC详解

前言 - CSS中的文档流 在介绍BFC之前&#xff0c;需要先给大家介绍一下文档流。​ 我们常说的文档流其实分为定位流、浮动流、普通流三种。​ ​ 1. 绝对定位(Absolute positioning)​ 如果元素的属性 position 为 absolute 或 fixed&#xff0c;它就是一个绝对定位元素。​ 在…

在 Spring Boot 中实时监控 Redis 命令流

前言 在 Redis 的日常使用和调试中&#xff0c;监控命令流有助于我们更好地理解 Redis 的工作状态。Redis 提供了 MONITOR 命令&#xff0c;可以实时输出 Redis 中所有客户端的命令请求&#xff0c;这一功能在调试和分析性能时非常有帮助。在 Spring Boot 项目中&#xff0c;我…

ReadKidz | 一个生成儿童故事绘本的平台

AI创作丨使用ReadKidz快速生成儿童故事绘本 ReadKidz 是一款AI平台&#xff0c;专为快速创作儿童故事绘本而设计。用户仅需输入简单提示词并根据喜好进行选择&#xff0c;便能生成精美的个性化绘本&#xff0c;适合家长、教师或创作者为孩子们创建有趣且富教育意义的故事。 使用…

aosp15系统窗口闪屏原生bug-dim图层相关-你会修改吗?

背景 近期各个大厂已经开始准备aosp15的系统rom适配工作了&#xff0c;应该是想2025年初开发发布相关的新机型&#xff0c;所以慢慢的我们也要开始适应aosp15版本的相关问题的修改和研究哈。 近期就有相关学员朋友在做android15相关的dialog开发时候&#xff0c;发现了一个严…