Java并发必知必会第三弹:用积木讲解ABA原理
Java并发必知必会第三弹:用积木讲解ABA原理
可落地的 Spring Cloud项目:PassJava
本篇主要内容如下
一、背景
上一节我们讲了程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单?,留了一个彩蛋给大家,ABA问题是怎么出现的,为什么不是AAB拖拉机,AAA金花,4个A炸弹 ?这一篇我们再来揭开ABA的神秘面纱。
二、面试连环炮
面试的时候我们也经常遭遇面试官的连环追问:
- CAS概念?
- Unsafe类是干啥用的?
- CAS底层实现是怎么样的
- ABA问题什么场景下会出现?
- ABA有什么危害?
- 原子引用更新是啥?
- 如何避免ABA问题?
三、用积木讲解ABA问题
案例:甲看见一个三角形积木,觉得不好看,想替换成五边形,但是乙想把积木替换成四边形。(前提条件,只能被替换一次)
可能出现的过程如上图所示:
- 第一步:乙先抢到了积木,将
三角形A
积木替换成五角星B1
- 第二步:乙将
五角星B1
替换成五边形B2
- 第三步:乙将
五边形B2
替换成棱形B3
- 第四步:乙将
棱形B3
替换成六边形B4
- 第五步:乙将
六边形B4
替换成三角形A
- 第六步:甲看到积木还是三角形,认为乙没有替换,甲可以进行替换
- 第七步:甲将
三角形V
替换成了五边形B
讲解:第一步道第五步,都是乙在替换,但最后还是替换成了三角形(即是不是同一个三角形),这个就是ABA,A指最开始是三角形,B指中间被替换的B1/B2/B3/B4,第二个A就是第五步中的A,中间不论经过怎么样的形状替换,最后还是变成了三角形。然后甲再将A2和A1进行形状比较,发现都是三角形,所以认为乙没有动过积木,甲可以进行替换。这个就是比较并替换(CAS)中的ABA问题。
小结:CAS只管开头和结尾,中间过程不关心,只要头尾相同,则认为可以进行修改,而中间过程很可能被其他人改过。
四、用原子引用类演示ABA问题
AtomicReference
:原子引用类
- 1.首先我们需要定义一个积木类
/**
积木类
* @author: 悟空聊架构
* @create: 2020-08-25
*/
class BuildingBlock {
String shape;
public BuildingBlock(String shape) {
this.shape = shape;
}
@Override
public String toString() {
return "BuildingBlock{" + "shape='" + shape + '}';
}
}
- 2.定义3个积木:三角形A,四边形B,五边形D
static BuildingBlock A = new BuildingBlock("三角形");
// 初始化一个积木对象B,形状为四边形
static BuildingBlock B = new BuildingBlock("四边形");
// 初始化一个积木对象D,形状为五边形
static BuildingBlock D = new BuildingBlock("五边形");
- 初始化原子引用类
static AtomicReference<BuildingBlock> atomicReference = new AtomicReference<>(A);
- 4.线程“乙”执行ABA操作
new Thread(() -> {// 初始化一个积木对象A,形状为三角形
atomicReference.compareAndSet(A, B); // A->B
atomicReference.compareAndSet(B, A); // B->A
},
- 5.线程“甲”执行比较并替换
new Thread(() -> {// 初始化一个积木对象A,形状为三角形
try {
// 睡眠一秒,保证t1线程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 可以替换成功,因为乙线程执行了A->B->A,形状没变,所以甲可以进行替换。
System.out.println(atomicReference.compareAndSet(A, D) + "\t" + atomicReference.get()); // true BuildingBlock{shape='五边形}
}, "甲").start();
输出结果:true BuildingBlock{shape='五边形}
小结:当线程“乙”执行ABA之后,线程“甲”比较后,发现预期值和当前值一致,将三角形替换成了五边形。
五、那ABA到底有什么危害?
我们看到乙不管怎么进行操作,甲看到的还是三角形,那甲当成乙没有改变积木形状 又有什么问题呢?
出现的问题场景通常是带有消耗类的场景,比如库存减少,商品卖出。
1.我们想象一下生活中的这个喝水场景:
(1)一家三口人,爸爸、妈妈、儿子。
(2)一天早上6点,妈妈给儿子的水杯灌满了水(水量为A),儿子先喝了一半(水量变成B)。
(3)然后妈妈把水杯又灌满了(水量为A),等中午再喝(妈妈执行了一个ABA操作)。
(4)爸爸7点看到水杯还是满的(不知道是妈妈又灌满的),于是给儿子喝了1/3(水量变成D)
(5)那在中午之前,儿子喝了1/2+1/3= 5/6的水,这不是妈妈期望的,因为妈妈只想让儿子中午之前喝半杯水。
这个场景的ABA问题带来的后果就是本来只用喝1/2的水,结果喝了5/6的水。
2.我们再想象一下电商中的场景
(1)商品Y的库存是10(A)
(2)用户m购买了5件(B)
(3)运营人员乙补货5件(A)(乙执行了一个ABA操作)
(4)运营人员甲看到库存还是10,就认为一件也没有卖出去(不考虑交易记录),其实已经卖出去了5件。
那我们怎么解决原子引用的问题呢?
可以用加版本号的方式来解决两个A相同的问题,比如上面的积木案例,我们可以给两个三角形都打上一个版本号的标签,如A1和A2,在第六步中,形状和版本号一致甲才可以进行替换,因形状都是三角形,而版本号一个1,一个是2,所以不能进行替换。
在Java代码中,我们可以用原子时间戳引用类型:AtomicStampedReference
六、带版本号的原子引用类型
1.我们看一看这个原子类AtomicStampedReference
的底层代码
比较并替换方法compareAndSet
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
expectedReference
:期望值
newReference
:替换值
expectedStamp
:期望版本号
newStamp
:替换版本号
先比较期望值expectedReference和当前值是否相等,以及期望版本号和当前版本号是否相等,如果两者都相等,则表示没有被修改过,可以进行替换。
2.如何使用AtomicStampedReference?
(1)先定义3个积木:三角形A,四边形B,五边形D
// 初始化一个积木对象A,形状为三角形
BuildingBlock A = new BuildingBlock("三角形");
// 初始化一个积木对象B,形状为四边形,乙会将三角形替换成四边形
BuildingBlock B = new BuildingBlock("四边形");
// 初始化一个积木对象B,形状为四边形,乙会将三边形替换成五边形
BuildingBlock D = new BuildingBlock("五边形");
(2)创建一个原子引用类型的实例 atomicReference
// 传递两个值,一个是初始值,一个是初始版本号
AtomicStampedReference<BuildingBlock> atomicStampedReference = new AtomicStampedReference<>(A, 1);
(3)创建一个线程“乙”执行ABA操作
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);
// 暂停线程“乙”1秒钟,使线程“甲”可以获取到原子引用的版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
* 乙线程开始ABA替换
* */
// 1.比较并替换,传入4个值,期望值A,更新值B,期望版本号,更新版本号
atomicStampedReference.compareAndSet(A, B, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp()); //乙 第一次版本号1
// 2.比较并替换,传入4个值,期望值B,更新值A,期望版本号,更新版本号
atomicStampedReference.compareAndSet(B, A, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); // 乙 第二次版本号2
System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp()); // 乙 第三次版本号3
}, "乙").start();
1)乙先获取原子类的版本号,第一次获取到的版本号为1
2)暂停线程“乙”1秒钟,使线程“甲”可以获取到原子引用的版本号
3)比较并替换,传入4个值,期望值A,更新值B,期望版本号stamp,更新版本号stamp+1。A被替换为B,当前版本号为2
4)比较并替换,传入4个值,期望值B,更新值A,期望版本号getStamp(),更新版本号getStamp()+1。B替换为A,当前版本号为3
(4)创建一个线程“甲”执行D替换A操作
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp); // 甲 第一次版本号1
// 暂停线程“甲”3秒钟,使线程“乙”进行一次ABA替换操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(A,D,stamp,stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + result + "\t 当前最新实际版本号:" + atomicStampedReference.getStamp()); // 甲 修改成功否false 当前最新实际版本号:3
System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值:" + atomicStampedReference.getReference()); // 甲 当前实际最新值:BuildingBlock{shape='三角形}
}, "甲").start();
(1)甲先获取原子类的版本号,版本号为1,因为乙线程还未执行ABA,所以甲获取到的版本号和乙获取到的版本号一致。
(2)暂停线程“甲”3秒钟,使线程“乙”进行一次ABA替换操作
(3)乙执行完ABA操作后,线程甲执行比较替换,期望为A,实际是A,版本号期望值是1,实际版本号是3
(4)虽然期望值和实际值都是A,但是版本号不一致,所以甲不能将A替换成D,这个就避免了ABA的问题。
小结: 带版本号的原子引用类可以利用CAS+版本号来比较变量是否被修改。
总结
本篇分析了ABA产生的原因,然后又列举了生活中的两个案例来分析ABA的危害。然后提出了怎么解决ABA问题:用带版本号的原子引用类AtomicStampedReference。
限于篇幅和侧重点,CAS的优化并没有涉及到,后续再倒腾这一块吧。另外AtomicStampedReference的缺点本篇本没有进行讲解,限于笔者的技术水平原因,并没有一一作答,期待后续能补上这一块的解答。
我是悟空,一只努力变强的码农!我要变身超级赛亚人啦!
另外可以搜索「悟空聊架构」或者PassJava666,一起进步!
我的GitHub主页,关注我的Spring Cloud
实战项目《佳必过》
公众号
Java并发必知必会第三弹:用积木讲解ABA原理的更多相关文章
- 脑残式网络编程入门(三):HTTP协议必知必会的一些知识
本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...
- 第5节:Java基础 - 必知必会(下)
第5节:Java基础 - 必知必会(下) 本小节是Java基础篇章的第三小节,主要讲述Java中的Exception与Error,JIT编译器以及值传递与引用传递的知识点. 一.Java中的Excep ...
- 必知必会之 Java
必知必会之 Java 目录 不定期更新中-- 基础知识 数据计量单位 面向对象三大特性 基础数据类型 注释格式 访问修饰符 运算符 算数运算符 关系运算符 位运算符 逻辑运算符 赋值运算符 三目表达式 ...
- 第4节:Java基础 - 必知必会(中)
第4节:Java基础 - 必知必会(中) 本小节是Java基础篇章的第二小节,主要讲述抽象类与接口的区别,注解以及反射等知识点. 一.抽象类和接口有什么区别 抽象类和接口的主要区别可以总结如下: 抽象 ...
- 第3节:Java基础 - 必知必会(上)
第3节:Java基础 - 必知必会(上) 本篇是基础篇的第一小节,我们从最基础的java知识点开始学习.本节涉及的知识点包括面向对象的三大特征:封装,继承和多态,并且对常见且容易混淆的重要概念覆盖和重 ...
- 必知必会之Java注解
必知必会之Java注解 目录 不定期更新中-- 元注解 @Documented @Indexed @Retention @Target 常用注解 @Deprecated @FunctionalInte ...
- Java面试必知必会:基础
面试考察的知识点多而杂,要完全掌握需要花费大量的时间和精力.但是面试中经常被问到的知识点却没有多少,你完全可以用 20% 的时间去掌握 80% 常问的知识点. 一.基础 包括: 杂七杂八 面向对象 数 ...
- Java面试必知必会(扩展)——Java基础
float f=3.4;是否正确? 不正确 3.4是双精度,将双精度赋值给浮点型属于向下转型,会造成精度损失: 因此需要强制类型转换: 方式一:float f=(float)3.4 方式二:float ...
- MySql必知必会实战练习(三)数据过滤
在之前的博客MySql必知必会实战练习(一)表创建和数据添加中完成了各表的创建和数据添加,MySql必知必会实战练习(二)数据检索中介绍了所有的数据检索操作,下面对数据过滤操作进行总结. 1. whe ...
随机推荐
- luogu P6570 [NOI Online #3 提高组]优秀子序列 二进制 dp
LINK:P6570 [NOI Online #3 提高组]优秀子序列 Online 2的T3 容易很多 不过出于某种原因(时间不太够 浪了 导致我连暴力的正解都没写. 容易想到 f[i][j]表示前 ...
- 4.23 子集 分数规划 二分 贪心 set 单峰函数 三分
思维题. 显然考虑爆搜.然后考虑n^2能做不能. 容易想到枚举中间的数字mid 然后往mid两边加数字 使其整个集合权值最大. 这里有一个比较显然的贪心就不再赘述了. 可以发现这样做对于集合是奇数的时 ...
- SSRS - 请求因 HTTP 状态 401 失败: Unauthorized。
原因: 1.SSRS报表服务器停止了,重启就可以了 2.用户没有权限 3.用户登录密码过期了,重设密码就可以了(如果用户是安装了client的话,直接让他远程登录改一下密码)
- 服务治理框架dubbo中zookeeper的使用
Zookeeper提供了一套很好的分布式集群管理的机制,就是它这猴子那个几月层次型的目录树的数据结构,并对书中的节点进行有效的管理,从而可以设计出多种多样的分布式的数据管理模型:下面简要介绍下zook ...
- .Net Core 3.0下AOP试水~~
昨天躺了一下3.0的依赖注入的雷 今天顺势把AOP做了一下调整,比如自动化的AOP注入 默认的Program里面的CreateHostBuilder方法增加一行 public static IHost ...
- 【NOIP2015四校联训Day7】 题 题解(Tarjan缩点+DFS)
前言:没错,这题的名字就这么直白.我们考试题. ------------------ 你需要完成$n$道题目.有一些题目是相关的,当你做一道题的时候,如果你做过之前对它有帮助的题目,你会更容易地做出它 ...
- java 封装与this关键字
一 封装 1.封装的概述 封装,它也是面向对象思想的特征之一.面向对象共有三个特征:封装,继承,多态. 封装表现: 1.方法就是一个最基本封装体. 2.类其实也是一个封装体. 从以上两点得出结论,封装 ...
- Python中json.dump与repr的区别
Json是一种轻量级的数据交换格式,Python3 中可以使用 json 模块来对 JSON 数据进行编解码,它包含了两个函数: 引入json包: import json json.dumps(): ...
- Name jms can't bind to context问题解决
需要把gis-datamanage包中的配置test改成compile
- 如何限制ip访问Oracle数据库
一.概述 本文将给大家介绍如何限制某个ip或某个ip段才能访问Oracle数据库 通过sqlnet.ora 通过/etc/hosts.deny和/etc/hosts.allow 通过iptables ...