使用 Koltin 集合时容易产生的 bug 注意事项
来看下面代码:
class ChatManager {private val messages = mutableListOf<Message>()/*** 当收到消息时回调*/fun onMessageReceived(message: Message) {messages.add(message)}/*** 当删除消息时回调*/fun onMessageDeleted(message: Message) {messages.remove(message)}/*** 当消息成功发送到服务器时回调*/fun onMessageDeliveryStateChanged(messageId: String, state: DeliveryState) {val messageIndex = messages.indexOfFirst { it.id == messageId }if (messageIndex >= 0) {val message = messages[messageIndex]messages[messageIndex] = message.copy(deliveryState = state)}}
}data class Message(val id: String,val content: String,val senderId: String,val receiverId: String,val deliveryState: DeliveryState
)
enum class DeliveryState { UNDELIVERED, SENT, DELIVERED }
上面代码中 ChatManager
持有一个 mutableListOf
类型的属性成员 messages
,ChatManager
主要负责在接收消息、删除消息、发送消息时对消息状态进行管理。
我们思考一下,这个代码有什么问题呢?
如果你只是在单线程/主线程中调用这个ChatManager
类的相关方法,那么不会有任何问题,但是假如你在多个线程中调用这个类,比如在线程池中跑,那就不一定了。
想必你大概已经猜到了,出现问题的原因就是多个线程的情况下,不同的线程调用不同的方法对 messages
进行操作可能导致资源竞争,因此这里有潜在的并发安全问题。
举个例子,假如在多线程环境下我们有以下代码:
val chatManager = ChatManager()
...
chatManager.onMessageDeleted(message) // Thread1 正在访问这一行
...
// 同时,Thread2 正在访问这一行
chatManager.onMessageDeliveryStateChanged("abc", DeliveryState.DELIVERED)
这时会有什么问题呢?
假设程序按照上图标注的顺序执行, messages
集合列表中此时共有 [A, B, C, D, E]
5个消息对象,那么线程 2 首先查询到 index = 3
的消息(也就是D
),此时线程 1 同时在执行 onMessageDeleted
方法,删除了消息 D
,这之后,线程 2 开始进入 if
代码块执行,此时线程 2 并不知道有其他人修改了 messages
集合,那么它会按照 index = 3
来取出消息并修改它的状态,但是由于消息列表中的 D
被线程 1 删除了,列表变成 [A, B, C, E]
,因此这时线程 2 取到的index = 3
的消息会是 E
,那么结果就是本应该修改 D
的状态却阴差阳错地修改了 E
的状态!这就很要命了!
还没有完,假如 messages
集合列表只有 [A, B, C, D]
4个消息,同样按照上面的逻辑分析你会发现线程 2 这时取不到 index = 3
的消息了,因为被线程 1 删除了一个,消息列表不够 4 个了,这种情况下,你的应用可能会得到某种类似于 IndexOutOfBoundsException
的异常信息,如果你没有捕获处理异常,那么恭喜你,你的应用此时崩溃了!
所以,如果你没有意识到集合类可能在多线程下导致的并发安全问题,一旦产生这样的bug或异常,就会很棘手,很难发现问题的原因。
有人可能会想到,既然 MutableList
有问题,那么我用不可变的 List
不就可以了(严格说是只读的),于是代码可能会修改成下面这样:
class ChatManagerFixed {private var messages = listOf<Message>()/*** 当收到消息时回调*/fun onMessageReceived(message: Message) {messages += message}/*** 当删除消息时回调*/fun onMessageDeleted(message: Message) {messages -= message}/*** 当消息成功发送到服务器时回调*/fun onMessageDeliveryStateChanged(messageId: String, state: DeliveryState) {messages = messages.map { message ->if (message.id == messageId) {message.copy(deliveryState = state)} else message}}
}
注意,messages += message
、messages -= message
这样的操作每次都会产生一个新的 List
对象,就像 Java 的 String
类那样,每次操作都会产生一个新的不可变String
对象,这样应该没有问题了吧?
但实际上这个代码仍然存在并发安全隐患,问题就在于 messages += message
,它其实等价于下面代码:
messages = messages + message
很明显,这不是一个原子操作,涉及到 messages
变量的一次读操作和 messages
变量的一次写操作。假设有多个线程同时执行这段代码,依然会存在同步问题:
fun onMessageReceived(message: Message) {// List is initially []// Thread 1 adds "Message 1"// Thread 2 adds "Message 2"// Expected: ["Message 1", "Message 2"]// If thread 1 finishes first, the list will be ["Message 1"]// If thread 2 finishes first, the list will be ["Message 2"]messages = messages + message
}
如上面代码注释所示,如果 List
初始为空,有 2 个线程同时往里面添加消息,那么可能结果不会按照我们的预期那样。
一旦理解了问题所在,解决办法就很简单了,从 Java 过来的我们肯定有着解决并发问题的丰富经验,比如最简单的就是使用 Kotlin 提供的同步工具 synchronized
函数:
class ChatManagerFixed {private val lock = Any()private var messages = listOf<Message>()/*** 当收到消息时回调*/fun onMessageReceived(message: Message) {synchronized(lock) {messages += message}}/*** 当删除消息时回调*/fun onMessageDeleted(message: Message) {synchronized(lock) {messages -= message}}/*** 当消息成功发送到服务器时回调*/fun onMessageDeliveryStateChanged(messageId: String, state: DeliveryState) {synchronized(lock) {messages = messages.map { message ->if (message.id == messageId) {message.copy(deliveryState = state)} else message}}}
}
当然,如果你喜欢用 MutableList
,也是一样的解决方式:
class ChatManagerFixed {private val lock = Any()private val messages = mutableListOf<Message>()/*** 当收到消息时回调*/fun onMessageReceived(message: Message) {synchronized(lock) {messages.add(message)}}/*** 当删除消息时回调*/fun onMessageDeleted(message: Message) {synchronized(lock) {messages.remove(message)}}/*** 当消息成功发送到服务器时回调*/fun onMessageDeliveryStateChanged(messageId: String, state: DeliveryState) {synchronized(lock) {val messageIndex = messages.indexOfFirst { it.id == messageId }if (messageIndex >= 0) {val message = messages[messageIndex]messages[messageIndex] = message.copy(deliveryState = state)}}}
}
可以看到这个问题的解决并非难事,非常简单,困难的是如何发现这种问题,如果没有并发安全的意识,可能只能对着应用抛出的异常日志发呆而无从下手。
如果你使用 Kotlin 协程,在 Kotlin 协程中也提供了一些相应的并发工具,如
Mutex
、Semaphore
等,感兴趣的可以参考我的另一篇文章:【深入理解Kotlin协程】协程中的Channel和Flow & 协程中的线程安全问题
相关文章:
使用 Koltin 集合时容易产生的 bug 注意事项
来看下面代码: class ChatManager {private val messages mutableListOf<Message>()/*** 当收到消息时回调*/fun onMessageReceived(message: Message) {messages.add(message)}/*** 当删除消息时回调*/fun onMessageDeleted(message: Message) {messages.r…...
CKA认证,开启您的云原生之旅!
在当今数字化时代,云计算已经成为企业和个人发展的关键技术。而获得CKA(Certified Kubernetes Administrator)认证,将是您在云原生领域迈出的重要一步。 CKA认证是由Kubernetes官方推出的权威认证,它旨在验证您在Kuber…...
基于springboot+vue的抗疫物资管理系统(前后端分离)
博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 主要内容:毕业设计(Javaweb项目|小程序|Pyt…...
nebula容器方式安装:docker 安装nebula到windows
感谢阅读 基础环境安装安装docker下载nebula 安装数据库命令行安装查询network nebula-docker-compose_nebula-net并初始化查询安装初始使用root(God用户类似LINUX的root) 关闭服务 安装UI 基础环境安装 安装docker 点我下载docker 下载nebula 数据…...
干洗行业上门预约解决方案,干洗店洗鞋店小程序开发;
互联网干洗店洗鞋店小程序,企业干洗方案,干洗行业小程序,上门取衣小程序,预约干洗小程序,校园干洗店小程序,工厂干洗店小程序,干洗店小程序开发; 一、干洗店洗鞋店小程序核心功能介绍: 1.(支持上门取送、送货到店、寄存网点、智能衣柜四种下单方式) 用户下单-上门取…...
【Spring Boot 3】【JPA】@ManyToOne 实现一对多单向关联
【Spring Boot 3】【JPA】@ManyToOne 实现一对多单向关联 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学…...
Mathematica学习笔记收纳
笔记 可以关注官方公众号 帮助文件 https://reference.wolfram.com/language/index.html.zh 南京大学的介绍 https://oi.nju.edu.cn/Mathematica/listm.htm...
java反射高级用列(脱敏+aop)
ClassUtils 、FieldUtils、MethodUtils、ReflectionUtils高级 List<String> list = new ArrayList<>(); Class<?> userClass = ClassUtils.getUserClass(list.getClass()); System.out.println(Collection.class.isAssignableFrom(userClass)); Class<?…...
C++函数对象包装器function类详解
函数对象包装器是对函数的封装,为函数对象提供一个容器,一个封装。C中现有的可调用实体的一种类型安全的包装(相对来说,函数指针的调用不是类型安全的),换句话说,函数对象包装器就是函数的容器。…...
SpringMVC 学习(八)之文件上传与下载
目录 1 文件上传 2 文件下载 1 文件上传 SpringMVC 对文件的上传做了很好的封装,提供了两种解析器。 CommonsMultipartResolver:兼容性较好,可以兼容 Servlet3.0 之前的版本,但是它依赖了 commons-fileupload …...
《低功耗方法学》翻译——附录A:睡眠晶体管设计
附录A:睡眠晶体管设计 休眠晶体管是PMOS或NMOS高VT晶体管,用于在待机模式下关闭设计部件的电源。PMOS休眠晶体管用于切换VDD电源,因此被称为“header开关”。NMOS休眠晶体管控制VSS电源,因此被称为“footer开关”。在90 nm及以下…...
How to implement multiple file uploads based on Swagger 3.x in Spring boot 3.x
How to implement multiple file uploads based on Swagger 3.x in Spring boot 3.x Projectpom.xmlOpenAPIConfigFileUploadControllerapplication.yaml Project pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://…...
spring boot 集成科大讯飞星火认知大模型
首先到官网https://console.xfyun.cn/services/aidoc申请key 一、安装依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&…...
springboot/ssm高校宣讲会管理系统Java企业招聘宣讲系统web
演示视频:https://www.bilibili.com/video/BV1vz421R7cg/、 基于springboot(可改ssm)vue项目 开发语言:Java 框架:springboot/可改ssm vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库&am…...
2024.02.23作业
1. 尝试处理普通信号 #include "test.h"#define MAXSIZE 128void handler(int signo) {if (SIGINT signo){printf("用户按下了 ctrl c 键\n");} }int main(int argc, char const *argv[]) {if (signal(SIGINT, SIG_IGN) SIG_ERR){perror("signal …...
倒模专用制作耳机壳UV树脂:改性丙烯酸树脂
倒模专用制作耳机壳的UV树脂是经过改性的丙烯酸树脂,具有高透明度、高粘度、快速固化的特点。这种树脂可以通过紫外线光固化,快速形成坚硬的表面,并且具有较高的硬度和耐磨性,因此非常适合用于制作耳机壳。 此外,改性丙…...
chatgpt:还有哪些人工智能和科技值得关注?
今天,很多人的目光都被ChatGPT吸引,其实,人工智能的范围很大,远不止ChatGPT或者其他自然语言的处理工具。所以说不管ChatGPT的结果如何,人工智能依然是未来。 那么在ChatGPT之外,还有没有什么值得关注的人…...
LeetCode 2997.使数组异或和等于K的最少操作次数
给你一个下标从 0 开始的整数数组 nums 和一个正整数 k 。 你可以对数组执行以下操作 任意次 : 选择数组里的 任意 一个元素,并将它的 二进制 表示 翻转 一个数位,翻转数位表示将 0 变成 1 或者将 1 变成 0 。 你的目标是让数组里 所有 元素…...
计算机设计大赛 深度学习大数据物流平台 python
文章目录 0 前言1 课题背景2 物流大数据平台的架构与设计3 智能车货匹配推荐算法的实现**1\. 问题陈述****2\. 算法模型**3\. 模型构建总览 **4 司机标签体系的搭建及算法****1\. 冷启动**2\. LSTM多标签模型算法 5 货运价格预测6 总结7 部分核心代码8 最后 0 前言 ǵ…...
WPF 附加属性+控件模板,完成自定义控件。建议观看HandyControl源码
文章目录 相关连接前言需要实现的效果附加属性添加附加属性,以Test修改FontSize为例依赖属性使用触发器使用直接操控 结论 控件模板,在HandyControl的基础上面进行修改参考HandyControl的源码控件模板原型控件模板 控件模板触发器完整样式简单使用 结论 …...
编程笔记 Golang基础 040 defer、panic 和 recover
编程笔记 Golang基础 040 defer、panic 和 recover 一、defer二、panic三、recover小结 在Go语言中,defer、panic 和 recover 是一组用于错误处理和控制程序流程的关键字。它们之间的交互有助于实现异常处理机制,并确保资源的正确释放。 一、defer defe…...
通过redfish协议实现服务器固件升级、从虚拟光驱启动自检盘并等待完成,最后截图保存
通过redfish协议实现服务器固件升级、从虚拟光驱启动自检盘并等待完成,最后截图保存 版本信息代码新开发的PCIE设备在做服务器适配时,有时需要服务器厂家更新BMC或BIOS固件。同时,我们也希望对PCIE设备做一些检测,最后收集一些信息存档。如果需要处理的服务器很多,通过BMC的界面…...
ARM 版银河麒麟桌面系统下 Qt 开发环境搭建指南
目录 前言安装Linux ARM 版 QtCreator配置 Qt Creator配置构建套件 第一个麒麟 Qt 应用程序小结 前言 在上一篇文章信创ARM架构QT应用开发环境搭建中建议大家使用 Ubuntu X86 系统作为信创 ARM 架构 QT 应用的开发环境,里面使用了交叉编译的方式。这对于自己的 Qt …...
架构面试题汇总:缓存(二)
目录 1. 问题:什么是缓存,以及为什么我们需要缓存?2. 问题:你能解释一下缓存击穿、缓存雪崩和缓存预热是什么吗?3. 问题:如何在Java中实现缓存?4. 问题:你如何决定哪些数据应该被缓存…...
【docker入门】1-
文章目录 参考: Docker – 容器虚拟化平台。 参考: docker入门,这一篇就够了。【零基础入门Docker】Dockerfile中的USER指令以及dockerfile命令详解dockerfile copy命令...
微信小程序-全局配置
个人笔记,仅供参考。 1.entryPagePath 代码: "entryPagePath": "pages/index/index" 具体用法: 2.pages 小程序中新增/减少页面,都需要对 pages 数组进行修改。 代码: "pages": [&…...
【Android】性能优化之内存、网络、布局、卡顿、安装包、启动速度优化
欢迎来到 Android 开发老生常谈的性能优化篇,本文将性能优化划分为内存、网络、布局、卡顿、安装包、启动速度七块,从这七块优化出发,阐述优化的 Application 的方式。 目录 内存优化避免内存泄漏使用内存分析工具优化数据结构和算法数据缓存…...
第3.6章:StarRocks数据导入——DataX StarRocksWriter
一、Datax 1.1 DataX 3.0概述 DataX3.0是一个异构数据源离线同步工具,可以方便的对各种异构数据源进行高效的数据同步。 其github地址为: https://github.com/alibaba/DataX/blob/master/introduction.mdhttps://github.com/alibaba/DataX/blob/mast…...
【非递归版】归并排序算法(2)
目录 MergeSortNonR归并排序 非递归&归并排序VS快速排序 整体思想 图解分析 代码实现 时间复杂度 归并排序在硬盘上的应用(外排序) MergeSortNonR归并排序 前面的快速排序的非递归实现,我们借助栈实现。这里我们能否也借助栈去…...
[C++]C++实现本地TCP通讯的示例代码
这篇文章主要为大家详细介绍了C如何利用TCP技术,实现本地ROS1和ROS2的通讯,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下 概要服务端代码 头文件源代码客户端代码 概要 利用TCP技术,实现本地ROS1和ROS2的通讯。 服务端代码 头文件 #include &…...
一网信息一个简单便捷的新闻网站/泰安seo
前文用Python实现CRUD功能REST服务中发现,一个普通的web.py页面每秒只能执行数十次requests,经网友Arbow提醒, web.py默认是单线程方式,所以性能提升困难,并推荐了一些高性能的web framework。同时也看到Python资深网友ZoomQuiet的…...
北京网站seo服务/百度一直不收录网站
更多代码请见:https://github.com/xubo245 基因数据处理系列 1.解释 RT,但是顺序一致 2.代码: hadoopMaster:~/disk2/xubo/project/alignment/SparkSW/SparkSW20161114/alluxio-1.3.0$ vi sparkSWSSWQ10.sh dir$(ls -l query/* |awk !/^d/ {print $…...
网站关键词做标签/百度免费安装下载
小白向单片机笔记七:电机前言一、电机驱动电机单极性步进电机原理图ULN2003D驱动芯片二、代码1.直流电机中断初始化函数 interrupt_utils.h主函数2.步进电机时延函数delay.h中断初始化函数interrupt_utils.h电机控制相关函数step_utils.h主函数前言 ULN2003D芯片&a…...
如何网站做淘客/宁波网站推广制作
原文链接http://www.cnblogs.com/zhouzhendong/p/8671759.html 题目传送门 - BZOJ3944 题意 多组数据(组数<10)。 每组数据一个正整数$n(n\leq 10^{10})$。 让你求$\sum_{i1}^{n}\varphi(i)$以及$\sum_{i1}^{n}\mu(i)$。 题解 杜教筛模版题。 杜教筛学习->传送…...
涿州住房和城乡建设局网站/关键词的优化和推广
上一篇文章分享了相机的自动对焦,了解了应该如何去选择对焦区域对焦点和如何使用相机的自动对焦模式。今天,要分享的是不少摄影者想要知道的问题:拍摄时,相机应该优先选择自动对焦,还是手动对焦?手动对焦上…...
产品发布会详细流程/德州seo优化
大家好,我是时间财富网智能客服时间君,上述问题将由我为大家进行解答。声卡用手机唱歌的方法:1、将手机重启。2、使用标配中带有一个开关的音频线连接直播手机和声卡,靠近开关的那头连接手机的耳机孔。3、将音频线的另一端连接在声…...