CAS为什么还存在线程安全问题(从所谓的ABA问题再学CAS)

概述

之前学习 CAS,从 Java 代码层面知道其原理,借助一条 CPU 原子指令,通过不断地自旋去比较(compare)和(and)赋值(set)。当时对线程安全的认知停留在将多条 Java 语句组合成一个原子操作,那么就能够保证线程安全。那想着 compare 对应 if,set 对应 =,而 compareAndSet() 又是 CPU 原子指令,那总不能为了学个 CAS 还去研究 CPU 指令吧,CAS 学完了。

直到看到有关 CAS 中 ABA 问题的文章,虽然这些文章对 ABA 问题举的例子普遍都有些烂(虽然 wiki 上面也是同样的例子,但烂就是烂),什么 B 线程将 value 值从 5 改到 6 再改回 5,哪个线程吃饱了没事干,要把一个值从 5 改到 6 再改回 5 呢?看这些文章也没弄明白 ABA 问题,或者说,我最初的疑惑是:compareAndSet 不是 CPU 原子命令吗,为什么 CAS 机制还存在线程安全(ABA)问题? 在弄清楚这个问题之后,感觉 ABA 问题就不是什么问题了。

CAS 机制为什么存在线程安全问题

先从问题出发,为什么 CAS 还存在线程安全问题?

Java内存模型

  1. 首先,需要简单了解 Java 内存模型(JMM),如图所示。

    数据保存在主内存中,线程会拷贝一份到自己的工作内存中作为缓存,所以需要使用 violate 关键字来修饰 value 变量保证可见性。可见性具体来说就是,任意一个线程修改其工作内存中的 value 时,会立即写回主内存,同时让其他线程中的缓存数据无效,其他线程如果需要使用 value,需要重新从主内存中读取最新值进行缓存。

  2. 其次,需要深度追问一下自己,compareAndSet 中 compare 比较的是哪两个对象? compareAndSet 中 set 赋值的是哪个对象? 后面通过 AtomicInteger 的源码简单看下,这里直接上结论:

    • compare 比较的两个对象是线程工作内存中的 value 和主内存中的 value;
    • set 赋值的对象是工作内存中的 value,通过 violate 来实时更新主内存(线程不能直接修改主内存的值)。
  3. 最后,为什么会有线程安全问题,问题发生的时间点在哪?

    compareAndSet 是 compre(比较)和 set(赋值)是原子性操作,但实际涉及到 get-compare -> set 这三个操作,其中 get 是指从主内存获取数据到线程的工作内存的过程。产生线程安全问题的核心就是 CAS 只保证 compare -> set 这两个过程是原子的,但是 get -> compare 并不是原子的,这个时间段(从主内存读取 value 值到实际执行 CAS 指令)会被其它线程趁虚而入。因此如果发生下面的场景,对于一般的多线程环境而言,便出现线程安全问题。但,这不是一般,这是 CAS! 在这里插入图片描述

    CAS 机制流程图

ABA 问题

上面的论断先放一放,先可能带有错误地论述一下个人理解的 ABA 问题和线程安全问题之间的关系。

所谓的 ABA 问题,个人理解成是线程安全问题的其中一种抽象描述,例如我们将上面的 B = B1 + B2,那么可以理解成一个线程的操作插入到另一个线程的两次操作之间,并最终影响到那个线程的执行结果。类似的例子还有:

  • 从数据库的事务角度来说,就是不满足事务的隔离性,事务 B 影响了事务 A。
  • 从分布式系统 CP 架构的数据库和缓存的双写一致性角度来说,假设采用 Read/Write Through 模式,将 A 看做是未命中 Redis 缓存的读请求,B 看做是写请求,那么所谓的 ABA 问题就是写请求在缓存中的更新结果被读请求覆盖,造成缓存和数据库的不一致。如果不考虑缓存自动过期,那么这个不一致的时间需要等到下一次写请求才能够更新缓存。

为什么说 ABA 是线程安全问题的其中一种描述,假设上图中,线程 A 的 CAS 操作在线程 B 的 CAS 操作之前,那么操作失败的就是线程 B 的 CAS 操作,同样线程 A 影响到线程 B,那这种模式可以描述为 ABAB,那 ABAB 可以看做是 ABA 的一种子模式嘛(ABA*),综上,个人认为 ABA 问题(狭义) = 线程安全问题(广义)。

CAS 的特性

从上面的流程图中可以看出,CAS 机制应该包括:compareAndSet 原子操作、自旋。自旋本质上就是重试,可以类比消息队列的消息重发,TCP 的报文重发等。

CAS 这种实现并发的机制,并没有像 synchronized、lock 等锁机制来真正避免线程安全问题,而是通过重试机制来逃避线程安全问题。系统出了问题就卸载重启呗,费那么大劲debug干啥,没错,这就是 CAS 的理念,所以说逃避问题也是解决问题的一种有效方式。

至此,就 CAS 本身的理念而言,不存在线程安全问题,只要按照 CAS 的要求来,完美架构怎么说完美。但是,世界不是完美的,至少计算机的世界不是完美的。问题出现在 compare 上,或者说出现在 get 上。 如果 get 从主内存拷贝到线程的工作内存不是浅拷贝,又或者 compare 可以向 equals() 方法一样进行重载,而不是和 == 一样只能比较地址,那 CAS 也就不存在问题了。

假设 Person value = new Person("root", 18);,那么 value 作为 Person 对象的引用,实际上保存的只是一个地址,而修改对象中的字段属性并不会造成地址发生改变,即 value 的值没有变化。因此线程 B 将 value.name = "admin"; 并不会去更新主内存中的 value,更不会让线程 A 中的 value 缓存失效。在大多数情况下,这可能会造成业务错误,因为在这种情况下,线程 A 和线程 B 都成功执行了 compareAndSet 操作,可能会从逃避错误变成隐藏错误。(一般正常情况下,是线程 B 成功,线程 A 重做)

这种可能隐藏错误的情况,和那个线程 B 将值从 5 变到 6 再变回 5 的案例本质上都是在论述一件事:线程 B 对 value 对象属性的修改没有被线程 A 的 CAS 操作发现。但这称之为错误,个人觉得不合适,因为如果 value 是基本数据类型,那这种特性就是最终一致性(高效的体现),也是 Redis 中 AOF 重写的理念,最后再次批斗一下那个 5/6/5 的案例。
在这里插入图片描述

处理方案

虽然上面嘴硬说那不是错误,那是特性,但是大多数情况下,这个"特性"还是要处理的。

解决办法: 添加一个自增的 int 类型数据(version)作为版本号,在判断的时候不仅判断 value 是否相等,还判断 version 是否相等。

疑问:是否可以通过只判断 version 呢?感觉一般情况下也 ok 啊,不会有让 int 越界然后重复的的并发量吧,有待进一步理解。

相似机制: HashMap、ArrayList 等通过 modCount 来禁止迭代时进行修改,虽然是记录修改次数,但将每一次修改就作为一个新版本来看待的话,也是一样的。

源码简析

public class AtomicInteger{
	private volatile int value;
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
        	// 通过Unsafe + 反射获取到value字段的偏移地址,所以valueOffset就是value在主内存中的地址,因此可以通知更新?
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    // compareAndSwap是原子指令,但CAS代表一套机制,并不是一个指令。而getAndSet及其源代码更能体现整个过程
	public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
}
public final class Unsafe {
    public final int getAndSetInt(Object o, long offset, int newValue) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, newValue));
        return v;
    }
}

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

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

相关文章

AI数字人盘活本地生活!

据艾瑞咨询统计,2022年中国本地生活服务市场规模达到3.8万亿元,同比增长23.5%。另据QuestMobile,2023年4月,本地生活综合服务行业全网渗透率38.4%,外卖服务渗透率15.6%。 本地生活市场仍具较大空间,各大平台…

ModStart框架助力博客开发,全新视频教程助你轻松上手!

它来了!它来了!ModStart框架的博客主题开发视频教程终于上线啦!如果你是一名开发者,想要快速搭建一个美观、功能强大的博客网站,那么这个教程将是你的不二之选! ModStart是一款基于Laravel的模块化开发框架…

Git版本控制系统:简介、演变与优缺点

目录 前言1 版本控制概述2 集中式版本控制的优缺点2.1 优点2.2 缺点 3 分布式版本控制的优缺点3.1 优点3.2 缺点 4 Git的发展过程结语 前言 在软件开发和团队协作中,版本控制是至关重要的。它允许开发人员跟踪文件的更改历史,协同工作并管理代码的不同版…

MLX vs MPS vs CUDA:苹果新机器学习框架的基准测试

如果你是一个Mac用户和一个深度学习爱好者,你可能希望在某些时候Mac可以处理一些重型模型。苹果刚刚发布了MLX,一个在苹果芯片上高效运行机器学习模型的框架。 最近在PyTorch 1.12中引入MPS后端已经是一个大胆的步骤,但随着MLX的宣布&#x…

神经科学与计算神经科学的蓬勃发展与未来趋势

导言 神经科学和计算神经科学是当前科学研究领域中备受关注的方向。本文将深入研究这两个领域的发展历程、遇到的问题、解决过程,以及未来的可用范围。我们还将关注在各国的应用现状以及未来的研究趋势,探讨如何在竞争中取胜,以及在哪些方面发…

PostgreSQL表中字段由字符串改为数组

需求:PostgreSQL数据库中的一张表的某些字段,之前存的是字符串,由于业务需求变更,需要存储多条数据,字段类型要改为数组,并保留原来的数据。 具体实现: 修改表中字段类型:把 etl_f…

全功能知识付费小程序系统源码是什么?有什么好处?

全功能知识付费小程序系统源码,是一个集课程管理、用户管理、支付管理、数据分析等于一体的综合性解决方案。它支持多种形式的课程内容,如视频、音频、图文等,满足不同用户的学习需求。同时,系统具备完善的支付功能,保…

Panoply查看nc文件的时间维

打开的是全球灌溉农田灌溉用水量遥感估算数据集(2011-2018),该文件以nc格式储存。nc格式文件就是一个多维的数据库。经纬度占了两维,可能还有时间维度,就是时空谱。 双击打开刚打开时只能看到2018年1月的灌溉数据 打…

快猫视频模板源码定制开发 苹果CMS 可打包成双端APP

苹果CMS快猫视频网站模板源码,可用于开发双端APP,后台支持自定义参数,包括会员升级页面、视频、演员、专题、收藏和会员系统等完整模块。还可以直接指定某个分类下的视频为免费专区,具备完善的卡密支付体系,无需人工管…

数合建模功能清单

在百度搜索 ”数合建模及可视化“,进入官网网站,有更详细的说明

k8s 中部署Jenkins

创建namespace apiVersion: v1 kind: Namespace metadata:name: jenkins创建pv以及pvc kind: PersistentVolume apiVersion: v1 metadata:name: jenkins-pv-volumenamespace: jenkinslabels:type: localapp: jenkins spec:#storageClassName: manualcapacity:storage: 5Giacc…

mysql部署 --(docker)

先查找MySQL 镜像 Docker search mysql ; 拉取mysql镜像,默认拉取最新的; 创建mysql容器,-p 代表端口映射,格式为 宿主机端口:容器运行端口 -e 代表添加环境变量,MYSQL_ROOT_PASSWORD是root用户…

ACE Tools环境配置指导

ACE Tools环境配置指导 简介: ACE Tools是一套为ArkUI-X应用开发者提供的命令行工具,支持在Windows/Ubuntu/macOS平台运行,用于构建OpenHarmony、HarmonyOS、Android和iOS平台的应用程序, 其功能包括开发环境检查,新建…

中缀表达式求值

中缀表达式是一种常见的数学表达式表示方法,它将操作符放在两个操作数的中间。例如,中缀表达式 "2 3" 表示将两个数相加。 中缀表达式求值的一般算法如下: 建立一个栈,用于存储操作数和运算符。从左到右扫描中缀表达…

如何做好买货查窜货这件事

窜货是大多数品牌都会遇到的渠道问题,店铺窜货是为了更多的利润空间,所以窜货还会伴随低价,治理窜货是品牌的义务,更是品牌的责任,品牌在管控渠道时应配合一套完整的控价流程,治理窜货也不例外,…

Vue 官方周报 #124 - 使用JSDoc记录组件属性

Hi &#x1f44b; 当你将鼠标悬停在IDE中的组件上时&#xff0c;显示组件属性所对应的描述&#xff0c;这个功能在开发过程中会很有用。你可以在传递给defineProps函数的TypeScript接口中使用JSDoc来实现这一点&#xff1a; MyComponent.vue <script setup lang"ts&…

Java 并发编程 —— Fork/Join 框架的原理详解

目录 一. 前言 二. 并发和并行 2.1. 并发 2.2. 并行 2.3. 分治法 三. ForkJoin 并行处理框架的理论 3.1. ForkJoin 框架概述 3.2. ForkJoin 框架原理 3.3. 工作窃取算法 四. ForkJoin 并行处理框架的实现 4.1. ForkJoinPool 类 4.2. ForkJoinWorkerThread 类 4.3.…

大华 DSS 城市安防数字监控系统 SQL 注入漏洞

漏洞简介 大华DSS数字监控系统itcBulletin接口对传入的数据没有预编译和充足的校验&#xff0c;导致该接口存在SQL注入漏洞&#xff0c;可通过注入漏洞获取数据库敏感信息。 资产测绘 app“dahua-DSS” 漏洞复现 POC: POST /portal/services/itcBulletin?wsdl HTTP/1.1 H…

图卷积神经网络发展

1. 图神经网络&#xff08;GNN&#xff09; 图神经网络的概念最早在2005年提出。2009年Franco博士在其论文 [2]中定义了图神经网络的理论基础。 本文中所提到的图均指图论中的图(Graph)。它是一种由若干个结点(Node)及连接两个结点的边(Edge)所构成的图形&#xff0c;用于刻画…

从源码到实践:深入了解鸿鹄电子招投标系统与电子招投标

在数字化采购领域&#xff0c;企业需要一个高效、透明和规范的管理系统。通过采用Spring Cloud、Spring Boot2、Mybatis等先进技术&#xff0c;我们打造了全过程数字化采购管理平台。该平台具备内外协同的能力&#xff0c;通过待办消息、招标公告、中标公告和信息发布等功能模块…