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

在这里插入图片描述

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();//设置用户信息到ThreadLocalcurrentUser.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;//帮助方法,用来获得一个指定元素数量模拟数据的ConcurrentHashMapprivate 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 -> {//下面的这段复合逻辑需要锁一下这个ConcurrentHashMapsynchronized (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.mzph.cn/news/882648.shtml

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

相关文章

【最新通知】2024年Cisco思科认证CCNA详解

CCNA现在涵盖安全性、自动化和可编程性。该计划拥有一项涵盖IT职业基础知识的认证&#xff0c;包括一门考试和一门培训课程&#xff0c;助您做好准备。 CCNA培训课程和考试最近面向最新技术和工作岗位进行了重新调整&#xff0c;为您提供了向任何方向发展事业所需的基础。CCNA认…

blender分离含有多个动作的模型,并导出含有材质的fbx模型

问题背景 笔者是模型小白&#xff0c;需要将网络上下载的fbx模型中的动作&#xff0c;分离成单独的动作模型&#xff0c;经过3天摸爬滚打&#xff0c;先后使用了blender&#xff0c;3d max&#xff0c;unity&#xff0c;最终用blender完成&#xff0c;期间参考了众多网络上大佬…

【Ansiable】ansible的模块和主机清单

目录 一、介绍一些运维自动化工具 二、Ansible 概述/简介 三、Ansible 工作机制 3.1 内部工作机制 3.2 外部工作机制 四、Ansible 执行流程 五、Ansblie 安装以及日常操作模块***** 5.1 ansible 环境安装部署 5.2 ansible 命令行模块 5.2.1 command 模块 5.2.2 shel…

明源云ERP报表服务GetErpConfig.aspx接口存在敏感信息泄露

一、漏洞简介 在访问 /service/Mysoft.Report.Web.Service.Base/GetErpConfig.aspx?erpKeyerp60 路径时&#xff0c;返回了包含敏感信息的响应。这些信息包括但不限于数据库连接字符串、用户名、密码、加密密钥等。这些敏感信息的暴露可能导致以下风险&#xff1a;数据库访问…

【IPv6】IPv6 NAT66介绍

参考链接 IPv6-to-IPv6 Network Address Translation (NAT66) (ietf.org)https://datatracker.ietf.org/doc/id/draft-mrw-nat66-00.html IPv6 NAT66 NAT66&#xff0c;全称为Network Address Translation for IPv6 to IPv6&#xff0c;是一种用于IPv6网络的地址转换技术。在…

Tkinter -- python GUI学习与使用

前言 python GUI 目前pythonGUI有很多&#xff0c;哪一个最好&#xff1f; 先说说我选择的思路&#xff0c;我的目的是开发一个易用的软件&#xff0c;最重要的是稳定&#xff0c;并且碰到问题能够解决&#xff0c;因此&#xff0c;我的目标很明确&#xff0c;有比较大的用户群…

基于Python的自然语言处理系列(39):Huggingface中的解码策略

在自然语言生成任务中&#xff0c;如何选择下一步的单词或者词语对生成的文本质量影响巨大。Huggingface 提供了多种解码策略&#xff0c;可以在不同的场景下平衡流畅度、创造力以及生成效率。在这篇文章中&#xff0c;我们将逐步介绍 Huggingface 中的几种常见解码策略&#x…

web API基础

作用和分类 作用: 就是使用 JS 去操作 html 和浏览器 分类&#xff1a; DOM (文档对象模型)、 BOM &#xff08;浏览器对象模型&#xff09; 什么是DOM DOM (Document Object Model) 译为文档对象模型&#xff0c;是 HTML 和 XML 文档的编程接口。 HTML DOM 定义了访问和操作 …

mingw64的Windows安装及配置教程gcc、g++等

mingw64.rar 链接&#xff1a;https://pan.baidu.com/s/18YrDRyi5NHtqnTwhJG6PuA 提取码&#xff1a;pbli &#xff08;免费永久有效&#xff0c;免安装&#xff0c;解压后配置环境变量即可使用&#xff09; 1 下载 解压后随便放到一个地方&#xff1a; 复制“bin”路径&am…

重磅:中国首个SVG技术标准发布,计育韬老师主笔起草

编辑搜图 中华人民共和国《融媒体 SVG 交互设计技术规范》是由复旦大学奇点新媒体研究中心等单位牵头&#xff0c;学科带头人计育韬等人主要起草&#xff0c;并于 2024 年 8 月起面向全社会行业从业者发布的最高技术标准。该标准前身为 2016 年计育韬与微信团队合作拟定的《SV…

置分辨率设置多显示器的时候提示, 某些设置由系统管理员进行管理

遇到的问题 设置分辨率设置多显示器的时候提示&#xff08;如下图所示&#xff09;&#xff1a; 某些设置由系统管理员进行管理 解决方法 先试试这个方法&#xff1a; https://answers.microsoft.com/zh-hans/windows/forum/all/%E6%9B%B4%E6%94%B9%E5%88%86%E8%BE%A8%E7%8…

强大的Python必备库,你知道几个?建议收藏!

在Python的世界里&#xff0c;库的丰富性让开发者的工作变得轻松而高效。那么&#xff0c;你知道哪些强大的Python必备库吗&#xff1f; 面对众多的Python库&#xff0c;如何选择适合自己的工具来提升开发效率和代码质量&#xff1f;&#xff0c;丰富多样的库如同一个个强大的…

AnaTraf | 提升网络性能:深入解析网络关键指标监控、TCP重传与TCP握手时间

AnaTraf 网络性能监控系统NPM | 全流量回溯分析 | 网络故障排除工具 在当今的数字化时代&#xff0c;网络的稳定性和性能对企业的运营效率至关重要。无论是内部通信、应用程序的运行&#xff0c;还是对外提供服务&#xff0c;网络都发挥着关键作用。对于网络工程师或IT运维人员…

EasyX图形库的安装

前言 EasyX是一个图形库&#xff0c;可以用来做一些c/c小游戏&#xff0c;帮助学习。 一、进入EasyX官网 https://easyx.cn/ 二、点击下载EasyX 三、下载好后以管理员身份运行它 四、点击下一步 五、然后它会自动检测你的编辑器&#xff0c;用哪个就在哪个点安装 六、安装成功…

【linux问题】Linux命令行终端窗口的输出,显示不全问题

Linux命令行终端窗口的输出&#xff0c;显示不全问题 问题&#xff1a; 图中显示的是一个Linux命令行终端窗口&#xff0c; nmcli dev wifi 是一个命令——列出所有能用的Wi-Fi。 执行命令后&#xff0c;窗口输出了显示了所有能用的Wi-Fi网络列表。 但是在每一行末尾有一个“…

KPaaS:基于微服务架构的低代码平台

基于微服务架构的低代码平台是当前企业数字化转型的重要工具。通过将微服务架构与低代码平台结合&#xff0c;可以实现高效、灵活且可扩展的应用开发。在当今数字化转型的浪潮中&#xff0c;企业面临着诸多挑战&#xff0c;其中IT资源有限以及对高效开发的需求尤为突出。KPaaS业…

AI一键生成钢铁是怎样炼成的ppt!用这2个工具轻松拿捏ppt制作!

钢铁是怎样炼成的&#xff0c;是中小学语文新课标必读书目&#xff0c;它是由前苏联作家尼古拉奥斯特洛夫斯基于1930年至1934年写成的半自传体长篇小说&#xff0c;全书详细记叙了保尔柯察金于20世纪初期的成长历程。 对于每个接受过九年义务教育的大小朋友来说&#xff0c;这…

漏洞挖掘JS构造新手向

前置思路文章 JS逆向混淆前端对抗 油猴JS逆向插件 JS加解密之mitmproxy工具联动Burp JS挖掘基础 伪协议 JavaScript伪协议是一种在浏览器中模拟网络请求的方法。它使用window.XMLHttpRequest对象或fetch()方法来模拟发送HTTP请求&#xff0c;而不是通过实际的网络请求来获…

碰到这个问题请更新或重新安装fastapi版本

ValueError: too many values to unpack (expected 2) ERROR: Exception in ASGI application Traceback (most recent call last) File "/usr/local/lib/python3.8/site-packages… Ubuntu&#xff0c;容器中&#xff0c; 碰到上面这个问题&#xff0c;请更新fastapi的版…

微信小程序——编写一个表白墙小程序

一、前期准备 注册微信小程序账号 在微信公众平台上注册小程序账号&#xff0c;获取小程序的 AppID。 安装开发工具 下载并安装微信开发者工具&#xff0c;用于小程序的开发、调试和预览。 二、界面设计 首页 展示最新的表白内容列表&#xff0c;每条表白可以包括发布者昵称、…