业务开发常见问题-并发工具类

news2024/11/27 18:33:44

在这里插入图片描述

hello,大家好,本讲我们一起聊一下常见的几个并发工具类的使用和坑!
在日常工作中,我们经常会遇到多线程并发问题,比如ThreadLocal、锁、ConcurrentHashMap、CopyOnWriteArrayList等。那么如何正常的使用呢?下面我们来一探究竟!

一、ThreadLocal

ThreadLocal 相信大家都很熟悉了,它是为了解决多线程的资源竞争问题的,比如两个线程同时访问同一个变量并修改它,我们需要保证两个线程不互相影响。是一种用于实现线程本地存储的工具类,允许你为每个线程创建和维护独立的变量副本。这样,每个线程都可以独立地改变它自己的副本,而不会影响其他线程的副本。这对于需要在多线程环境中保持状态的情况特别有用,而又不希望使用同步机制来共享状态。
废话不多说,直接上代码~
比如我们有一个 SpringBoot 的 Web 项目,使用 ThreadLocal 来保存用户上下文信息。
代码如下:

@RequestMapping
@RestController
public class ThreadLocalController {

    private static final ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);

    @GetMapping("wrong")
    public Map wrong(@RequestParam("userId") Integer userId) {
        //设置用户信息之前先查询一次ThreadLocal中的用户信息
        String before = Thread.currentThread().getName() + ":" + currentUser.get();
        //设置用户信息到ThreadLocal
        currentUser.set(userId);
        // 设置用户信息之后再查询一次ThreadLocal中的用户信息
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        //汇总输出两次查询结果
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    }
}

定义一个ThreadLocal类型的变量currentUser用于存储用户信息,在wrong方法中,在设置请求的用户 id 之前和之后分别获取一次currentUser存储的用户信息,并将结果返回。
配置文件:

server.tomcat.threads.max=1

将 tomcat 线程池的最大线程数设置成 1,原因后面再解释。

第一次请求http://localhost:8080/wrong?userId=1
请求结果:

{
"before": "http-nio-8080-exec-1:null",
"after": "http-nio-8080-exec-1:1"
}

符合预期,因为在设置用户之前,currentUser中是没有值的。
第二次请求http://localhost:8080/wrong?userId=2
请求结果:

{
"before": "http-nio-8080-exec-1:1",
"after": "http-nio-8080-exec-1:2"
}

我们看这个请求结果就出现问题了,按理说 before 应该也是 null,若是用户 1 的话,那我们在业务中通过currentUser中存储的用户操作数据时,数据上体现的操作人和实际操作人不一致。
为什么会产生这种问题呢?
springboot 程序是运行在 tomcat 上的,而 tomcat 中是有线程池来处理这些请求的(为了提高效率,避免频繁创建销毁线程),前面设置了server.tomcat.threads.max=1,也就是将 tomcat 最大线程设置为 1,所有的请求都是通过这个线程执行的。而ThreadLocal类型的变量currentUser是数据线程级别的,在第一次请求后,线程并没有被销毁,而是归还到了线程池中,也就是线程中的变量还是存在的。所以第二次请求时就可以获取到第一次请求设置的变量。所以我们在使用 ThreadLocal 是注意的点:
使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据

另外除了获取数据混乱的问题外,还可能导致内存泄漏问题,如每次请求都往ThreadLocal变量中放到数据,一直没有得到清空,从而导致内存泄漏。

正确的使用方式:

@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

二、ConcurrentHashMap

ConcurrentHashMap 是 Java 中的一个线程安全的哈希表实现,用于在多线程环境下高效地存储和检索键值对。它是 java.util.concurrent 包的一部分,设计用于替代传统的 Hashtable 和同步包装的 HashMap(通过 Collections.synchronizedMap 生成的同步 Map)。

主要特点

  • 高效并发:ConcurrentHashMap 允许多个线程并发地读写数据,而不会发生线程间的冲突。它通过分段锁(在 Java 8 之前)或 CAS 操作(在 Java 8 及之后)来实现高效的并发访问。

  • 无锁读取:读取操作通常不需要加锁,能够在不锁定整个数据结构的情况下进行并发读取。

  • 部分锁定:在 Java 8 之前,ConcurrentHashMap 使用分段锁(Segment)来减少锁的粒度。每个 Segment 是一个小的哈希表,只有在写操作时才需要锁定特定的 Segment。Java 8 之后,ConcurrentHashMap 使用了更为精细化的锁机制,结合 CAS 操作来进一步提高并发性能。

  • 不允许 null 键或值:与 HashMap 不同,ConcurrentHashMap 不允许存储 null 键或 null 值。

特别要注意的是:ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。
下面我们来解释下这句话。

假如有这样一个场景,map 可以存入 1000 个数据,现在map 中已有 900 个数据,现在用 10 个线程往 map 中插入数据,每次插入之前,先查询下 map 中还需要多少数据,然后放入。此时很多同学觉得使用ConcurrentHashMap可以解决这个所谓的线程并发问题,其实不然,上代码:

@RequestMapping("chm")
@RestController
public class CHMController {

    Logger log = LoggerFactory.getLogger(CHMController.class);

    //线程个数
    private static int THREAD_COUNT = 10;
    //总元素数量
    private static int ITEM_COUNT = 1000;

    //帮助方法,用来获得一个指定元素数量模拟数据的ConcurrentHashMap
    private ConcurrentHashMap<String, Long> getData(int count) {
        return LongStream.rangeClosed(1, count)
                .boxed()
                .collect(Collectors.toConcurrentMap(i -> UUID.randomUUID().toString(), Function.identity(),
                        (o1, o2) -> o1, ConcurrentHashMap::new));
    }

    @GetMapping("wrong")
    public String wrong() throws InterruptedException {
        ConcurrentHashMap<String, Long> concurrentHashMap = getData(ITEM_COUNT - 100);
        //初始900个元素
        log.info("init size:{}", concurrentHashMap.size());

        ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
        //使用线程池并发处理逻辑
        forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
            //查询还需要补充多少个元素
            int gap = ITEM_COUNT - concurrentHashMap.size();
            log.info("gap size:{}", gap);
            //补充元素
            concurrentHashMap.putAll(getData(gap));
        }));
        //等待所有任务完成
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
        //最后元素个数会是1000吗?
        log.info("finish size:{}", concurrentHashMap.size());
        return "OK";
    }

}

运行结果:

2024-10-21T18:30:35.789+08:00  INFO 8228 --- [demo] [nio-8080-exec-1] com.csdn.demo.controller.CHMController   : finish size:1700
2024-10-21T18:35:43.793+08:00  INFO 8228 --- [demo] [nio-8080-exec-1] com.csdn.demo.controller.CHMController   : init size:900
2024-10-21T18:35:43.794+08:00  INFO 8228 --- [demo] [Pool-2-worker-1] com.csdn.demo.controller.CHMController   : gap size:100
2024-10-21T18:35:43.794+08:00  INFO 8228 --- [demo] [Pool-2-worker-4] com.csdn.demo.controller.CHMController   : gap size:100
2024-10-21T18:35:43.794+08:00  INFO 8228 --- [demo] [Pool-2-worker-3] com.csdn.demo.controller.CHMController   : gap size:100
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-5] com.csdn.demo.controller.CHMController   : gap size:15
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-1] com.csdn.demo.controller.CHMController   : gap size:0
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-2] com.csdn.demo.controller.CHMController   : gap size:100
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-6] com.csdn.demo.controller.CHMController   : gap size:100
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-1] com.csdn.demo.controller.CHMController   : gap size:-181
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-7] com.csdn.demo.controller.CHMController   : gap size:-300
2024-10-21T18:35:43.795+08:00  INFO 8228 --- [demo] [Pool-2-worker-5] com.csdn.demo.controller.CHMController   : gap size:-315
2024-10-21T18:35:43.796+08:00  INFO 8228 --- [demo] [nio-8080-exec-1] com.csdn.demo.controller.CHMController   : finish size:1415

通过运行结果可以看到,最后concurrentHashMap变量存入的数据为 1415 个,而不是预期的 1000 个。

  • 初始大小为 900 个,正确
  • 每个线程查出来缺少的数据有 100 的,有 15 的,还有负数的,显然是不对的
  • 最后 map 中存入的总数是 1415

造成这种结果的原因是查询缺少多少个和添加数据操作不是原子的,这就解释了上面说的ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的。

解决方法也比较简单:那就是加锁synchronized

@GetMapping("right")
public String right() throws InterruptedException {
    ConcurrentHashMap<String, Long> concurrentHashMap = getData(ITEM_COUNT - 100);
    log.info("init size:{}", concurrentHashMap.size());


    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
        //下面的这段复合逻辑需要锁一下这个ConcurrentHashMap
        synchronized (concurrentHashMap) {
            int gap = ITEM_COUNT - concurrentHashMap.size();
            log.info("gap size:{}", gap);
            concurrentHashMap.putAll(getData(gap));
        }
    }));
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);


    log.info("finish size:{}", concurrentHashMap.size());
    return "OK";
}

这样做虽然可以解决原子问题,但是并不能发挥出ConcurrentHashMap自身的能力。
其实我们可以使用ConcurrentHashMap提供的原子性方法 computeIfAbsent,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,如果存在则通过increment方法加 1

private Map<String, Long> gooduse() throws InterruptedException {
    ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
        String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
                //利用computeIfAbsent()方法来实例化LongAdder,然后利用LongAdder来进行线程安全计数
                freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
            }
    ));
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    //因为我们的Value是LongAdder而不是Long,所以需要做一次转换才能返回
    return freqs.entrySet().stream()
            .collect(Collectors.toMap(
                    e -> e.getKey(),
                    e -> e.getValue().longValue())
            );
}

这种方式提升了性能(比synchronized)。原因在于computeIfAbsent底层使用 Java 自带的 Unsafe 实现的 CAS。它在虚拟机层面确保了写入数据的原子性,比加锁的效率高得多。

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

三、CopyOnWriteArrayList

最后我们简单的说说CopyOnWriteArrayList。从名字也可以看出来,它的原理就是写时复制。但是使用不当可能会造成严重的性能问题。因为很多同学只知道他是写时复制,却忽略了一个场景,那就是它适用于读多写少的场景,为什么呢?我们来看它的源码:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        synchronized (lock) {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        }
    }

在添加元素时,先复制出一个数组,元素添加到复制出来的数组中,最后在重新设置回去,而复制数据这一步时非常耗时的。所以当我们的场景是读多写少时,可以使用CopyOnWriteArrayList来解决线程安全问题。

好啦,以上就是本篇文章要介绍的内容了,欢迎小伙伴们一起讨论!!!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

P7400 [COCI2020-2021#5] Magenta 题解

#1024程序员节&#xff5c;征文# 人生中的第二道紫题。。。 题目传送门 解题思路 下文中的距离指的是 a , b a,b a,b 之间的边的数量。 Sub 2 即所有边 Paula 与 Marin 都可以行走。 根据题意 Paula 先手。因此&#xff0c;如果一开始 Paula 动不了&#xff0c;那么 M…

浏览器的渲染过程

文章目录 什么是浏览器的渲染&#xff1f;浏览器渲染过程面试问点&#xff1a;为什么操作DOM慢&#xff1f;回流与重绘那么&#xff0c;什么情况下会触发回流&#xff1f; 浏览器的优化 什么是浏览器的渲染&#xff1f; 简单的说就是浏览器将 HTML 代码解析出来&#xff0c;把…

轻松学会!回收站数据恢复的几种妙招

回收站数据恢复方法是一个涉及计算机操作和数据安全的重要话题。在日常使用电脑的过程中&#xff0c;我们经常会遇到误删文件或清空回收站的情况&#xff0c;导致重要数据丢失。幸运的是&#xff0c;有多种方法可以尝试恢复这些丢失的数据。以下将详细介绍几种常见的回收站数据…

C++: C/C++内存管理

前言 本篇博客将详细介绍C的内存管理 &#x1f496; 个人主页&#xff1a;熬夜写代码的小蔡 &#x1f5a5; 文章专栏&#xff1a;C 若有问题 评论区见 &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 ​ 一.C/C内存分布 让我们先来看看下面的代码吧 int globalVar 1; st…

【植物识别系统】Python+人工智能+深度学习+卷积神经网络算法+TensorFlow+算法模型+Django网页界面平台

一、介绍 植物识别系统&#xff0c;使用Python作为主要编程语言开发&#xff0c;通过收集常见的6中植物树叶&#xff08;‘广玉兰’, ‘杜鹃’, ‘梧桐’, ‘樟叶’, ‘芭蕉’, ‘银杏’&#xff09;图片作为数据集&#xff0c;然后使用TensorFlow搭建ResNet50算法网络模型&am…

C++:模板的特化与分离编译

之前我们在介绍模板的时候仅仅是简单的介绍了模板的用法&#xff0c;本篇文章我们来详细的介绍下模板中比较重要的几个点。 一&#xff0c;非类型模板参数 我们之前的c中&#xff0c;会将经常使用的而又确保在我们程序的运行过程中值不会改变的值进行#define&#xff1a; #d…

Unity Apple Vision Pro 保姆级开发教程-环境配置、导入 PolySpatial 案例、程序发布到设备

视频教程 Unity 环境配置、导入 PolySpatial 案例、程序发布到设备 Unity Vision Pro 中文课堂教程地址&#xff1a; Unity3D Vision Pro 开发教程【保姆级】 | Unity 中文课堂 教程说明 这期教程我将介绍使用 Unity 开发 Apple Vision Pro 应用所需要的 Unity 环境配置&…

055_基于python摄影平台交流系统

目录 系统展示 开发背景 代码实现 项目案例 获取源码 博主介绍&#xff1a;CodeMentor毕业设计领航者、全网关注者30W群落&#xff0c;InfoQ特邀专栏作家、技术博客领航者、InfoQ新星培育计划导师、Web开发领域杰出贡献者&#xff0c;博客领航之星、开发者头条/腾讯云/AW…

Android compose 重建流程1

前言 本文是笔者学习Compose是如何自动触发UI刷新的笔记,可能缺乏一定可读性和教导性.(建议阅读参考文献更具启发性) 使用以下BOM作为研究环境. composeBom "2024.04.01" androidx-compose-bom { group "androidx.compose", name "compose-bom…

实习冲刺Day2

算法题 反转链表 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}*…

AI大模型应用(3)开源框架Vanna: 利用RAG方法做Text2SQL任务

AI大模型应用(3)开源框架Vanna: 利用RAG方法做Text2SQL任务 RAG&#xff08;Retrieval-Augmented Generation&#xff0c;如下图所示&#xff09;检索增强生成&#xff0c;即大模型LLM在回答问题时&#xff0c;会先从大量的文档中检索出相关信息&#xff0c;然后基于这些检索出…

【LLaMA-Facrory】【模型评估】:代码能力评估——Qwen-Coder-7B 和 deepseek-coder-7b-base-v1.5

目录 序言 1 拉取 Qwen2.5-Coder-7B 模型 2 编写python测试模型 3 启动webui导入模型测试 4 模型评估 4.1 前期准备工作 4.2 Qwen2.5-Coder-7B 模型评估 数据说明 综合分析 4.3 deepseek-coder-7b-base-v1.5 模型评估 数据说明 综合分析 4.4 模型比较 1. 文本生成…

软件安全测试报告如何编写?CMA、CNAS软件安全测试机构推荐

随着软件产品的增多&#xff0c;产品安全成为软件企业留住用户的方法之一。安全测试是验证和检查软件安全的重要手段&#xff0c;而软件安全测试报告则是测试人员工作成果的最好体现&#xff0c;那么软件安全测试报告该如何编写呢?权威的CMA、CNAS软件安全测试机构又有哪些? …

WeMapEngine开发实战,创建你的第一个GIS项目

我们在《WeMapEngine可快速构建的GIS应用功能》一文中为你分享了WeMapEngine可快速建的GIS应用功能。 今天再为你分享基于WeMapEngine的开发实战&#xff0c;演示如何快速创建第一个GIS项目。 创建你的第一个GIS项目 现在&#xff0c;我们开始构建第一个项目。 在这个项目中…

emulator -version报错解决方案

使用android studio安装安卓环境之后&#xff0c;会发现emulator -version报如下错 [14960]:ERROR:android/android-emu/android/qt/qt_setup.cpp:28:Qt library not found at ..\emulator\lib64\qt\lib Could not launch C:\Users\litbai\..\emulator\qemu\windows-x86_64\qe…

【Java小白图文教程】-04-循环结构

精品专题&#xff1a; 01.《C语言从不挂科到高绩点》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12753294.html?spm1001.2014.3001.5482 02. 《SpringBoot详细教程》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12789841.html?spm1001.20…

深入理解计算机系统--计算机系统漫游

对于一段最基础代码的文件hello.c&#xff0c;解释程序的运行 #include <stdio.h>int main() {printf ( "Hello, world\n") ;return 0; }1.1、信息就是位上下文 源程序是由值 0 和 1 组成的位&#xff08;比特&#xff09;序列&#xff0c;8 个位被组织成一组…

springboot048校园资料分享平台(论文+源码)_kaic

校园资料分享平台 摘要 随着信息互联网购物的飞速发展&#xff0c;国内放开了自媒体的政策&#xff0c;一般企业都开始开发属于自己内容分发平台的网站。本文介绍了校园资料分享平台的开发全过程。通过分析企业对于校园资料分享平台的需求&#xff0c;创建了一个计算机管理校园…

无人机的电池放电详解!

一、定义与表示方法 无人机的电池放电率是指电池在一定时间内放出其储存电能的能力&#xff0c;这一参数通常用C数来表示。C数越大&#xff0c;表示放电速率越快。 例如&#xff0c;一个2C的电池可以在1/2小时内放完其全部电量&#xff0c;而一个10C的电池则可以在1/10小时内…

喜讯!望繁信科技荣膺2022年中国超自动化先锋企业TOP20

36氪重磅发布「2022中国超自动化先锋企业」调研结果。凭借多年在流程挖掘领域过硬的技术实力和突出的产品创新力&#xff0c;望繁信科技在众多调研样本企业中脱颖而出&#xff0c;赢得了专家评委的充分认可&#xff0c;成功入选2022年中国超自动化先锋企业TOP20。 什么是超自动…