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;}
}

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

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

相关文章

MySQL实战45讲课后问题

1、第一章 如果表T中没有字段k,而你执行了这个语句 select *fromTwhere k1, 那肯定是会报“不存在这个列”的错误: “Unknown column ‘k’ in ‘where clause’”。你觉得这个错误是在我们上面提到的哪个阶段报出来的呢? 解答:…

STL stack练习

CSTL之stack栈容器 - 数据结构教程 - C语言网CSTL之stack栈容器1.再谈栈回顾一下之前所学的栈,栈是一种先进后出的数据结构,而实现方式需要创建多个结构体,通过链式的方式进行实现,这是标准的栈的思路,而在STL中栈可以…

中央空调冷却塔循环水全自动加药装置PH电导率设备工作原理动画

一:全自动加药装置【概述】 随着在给水、排水处理过程中,常常投加各类化学药剂作为阻垢、杀菌灭藻、混凝、絮凝用,以达到净化水的目的。这些药剂有固体颗粒、液体,在投加过程中必须溶解、稀释及按配比定量投加方能取得最佳效果&am…

数据安全扫描仪荣膺网络安全优秀创新成果大赛优胜奖 - 凸显多重优势

近日,由中国网络安全产业联盟(CCIA)主办、CCI数据安全工作委员会中国电子技术标准化研究院等单位承办的“2023年网络安全优秀创新成果大赛”获奖名单公布。天空卫士数据安全扫描仪(DSS)产品获得创新成果大赛优胜奖。 本…

jenkins入门

文章目录 前言一、 jenkins的安装二、新建简单任务总结 前言 本篇文章是 jenkins 的入门级别案例,包括安装、基础概念介绍、新建简单任务 一、 jenkins的安装 下载 jenkins https://www.jenkins.io/download/ 当前案例下载的是 2.426.2 LTS 版本 下载安装jdk11 …

Leetcode—75.颜色分类【中等】

2023每日刷题&#xff08;六十五&#xff09; Leetcode—75.颜色分类 实现代码 class Solution { public:void sortColors(vector<int>& nums) {int red 0, white 0, blue 0;for(auto num: nums) {if(num 0) {red;} else if(num 1) {white;} else {blue;}}for…

Modbus转Profinet网关如何选型应该注意哪些事项

针对当今市场上众多Modbus转Profinet网关&#xff0c;怎样才能选择适合现场使用的Modbus转Profinet网关呢&#xff01; 在进行网关选型之前&#xff0c;一定要清楚设备支持哪些协议&#xff0c;如Modbus或Profinet&#xff0c;ModbusRTU&#xff0c;ModbusTCP;再或者是CANOPEN&…

pbootcms模板(自适应手机端)响应式相册图片网站模板 图片壁纸类网站源码下载

附详细搭建教程&#xff0c;赠送运营资料&#xff1a;SEO教程&#xff0c;友链交换&#xff0c;站长提交收录&#xff0c;优化推广联盟&#xff0c;自媒体等各大优质运营平台 (自适应手机端)响应式相册图片网站模板 图片壁纸类网站源码下载 PbootCMS内核开发的网站模板&#…

博客3万访问量了……

博客有3万访问量了呢。自从第一次用了赠送的1500的流量券&#xff0c;粉丝了从零突破了&#xff0c;到现在有150个粉丝了。 之前预想的写博客的初衷&#xff0c;也是记录自己的学习过程&#xff0c;毕竟好记忆不如烂笔头&#xff0c;记录下来就是长长久久的&#xff0c;随时可以…

图片编辑文字用什么软件?带你了解这5个

图片编辑文字用什么软件&#xff1f;在当今数字化的时代&#xff0c;图片编辑已经成为我们日常生活中不可或缺的一部分。有时候&#xff0c;我们需要在图片上添加文字&#xff0c;以增强图片的视觉效果或传达特定的信息。那么&#xff0c;有哪些可以在图片上编辑文字的软件呢&a…

快速解决msvcr100.dll丢失的方法分享,亲测有效的5个方法

在计算机使用过程中&#xff0c;我们可能会遇到各种问题&#xff0c;其中之一就是某些程序无法正常运行&#xff0c;提示“msvcr100.dll丢失”的错误信息。msvcr100.dll是Microsoft Visual C 2010运行库的一部分&#xff0c;它提供了许多常用的函数和类&#xff0c;这些函数和类…

CCF编程能力等级认证GESP—C++6级—20230923

CCF编程能力等级认证GESP—C6级—20230923 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09;判断题&#xff08;每题 2 分&#xff0c;共 20 分&#xff09;编程题 (每题 25 分&#xff0c;共 50 分)小杨买饮料小杨的握手问题 答案及解析单选题判断题编程题1编程题…