使用 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的源码控件模板原型控件模板 控件模板触发器完整样式简单使用 结论 …...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...

cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...

【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...

iview框架主题色的应用
1.下载 less要使用3.0.0以下的版本 npm install less2.7.3 npm install less-loader4.0.52./src/config/theme.js文件 module.exports {yellow: {theme-color: #FDCE04},blue: {theme-color: #547CE7} }在sass中使用theme配置的颜色主题,无需引入,直接可…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...