号外:可落地的 Spring Cloud项目:PassJavajava
上一节咱们讲了程序员深夜惨遭老婆鄙视,缘由竟是CAS原理太简单?,留了一个彩蛋给你们,ABA问题是怎么出现的,为何不是AAB拖拉机,AAA金花,4个A炸弹 ?这一篇咱们再来揭开ABA的神秘面纱。git
面试的时候咱们也常常遭遇面试官的连环追问:程序员
案例:甲看见一个三角形积木,以为很差看,想替换成五边形,可是乙想把积木替换成四边形。(前提条件,只能被替换一次)github
可能出现的过程如上图所示:面试
三角形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只管开头和结尾,中间过程不关心,只要头尾相同,则认为能够进行修改,而中间过程极可能被其余人改过。小程序
AtomicReference
:原子引用类安全
/** 积木类 * @author: 悟空聊架构 * @create: 2020-08-25 */
class BuildingBlock {
String shape;
public BuildingBlock(String shape) {
this.shape = shape;
}
@Override
public String toString() {
return "BuildingBlock{" + "shape='" + shape + '}';
}
}
复制代码
static BuildingBlock A = new BuildingBlock("三角形");
// 初始化一个积木对象B,形状为四边形
static BuildingBlock B = new BuildingBlock("四边形");
// 初始化一个积木对象D,形状为五边形
static BuildingBlock D = new BuildingBlock("五边形");
复制代码
static AtomicReference<BuildingBlock> atomicReference = new AtomicReference<>(A);
复制代码
new Thread(() -> {// 初始化一个积木对象A,形状为三角形
atomicReference.compareAndSet(A, B); // A->B
atomicReference.compareAndSet(B, A); // B->A
},
复制代码
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='五边形}markdown
**小结:**当线程“乙”执行ABA以后,线程“甲”比较后,发现预期值和当前值一致,将三角形替换成了五边形。架构
咱们看到乙无论怎么进行操做,甲看到的仍是三角形,那甲当成乙没有改变积木形状 又有什么问题呢?
出现的问题场景一般是带有消耗类的场景,好比库存减小,商品卖出。
(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的水。
(1)商品Y的库存是10(A)
(2)用户m购买了5件(B)
(3)运营人员乙补货5件(A)(乙执行了一个ABA操做)
(4)运营人员甲看到库存仍是10,就认为一件也没有卖出去(不考虑交易记录),其实已经卖出去了5件。
那咱们怎么解决原子引用的问题呢?
能够用加版本号的方式来解决两个A相同的问题,好比上面的积木案例,咱们能够给两个三角形都打上一个版本号的标签,如A1和A2,在第六步中,形状和版本号一致甲才能够进行替换,因形状都是三角形,而版本号一个1,一个是2,因此不能进行替换。
在Java代码中,咱们能够用原子时间戳引用类型:AtomicStampedReference
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和当前值是否相等,以及指望版本号和当前版本号是否相等,若是二者都相等,则表示没有被修改过,能够进行替换。
(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
实战项目《佳必过》
你好,我是
悟空哥
,「7年项目开发经验,全栈工程师,开发组长,超喜欢图解编程底层原理」。
我还手写了 2 个小程序
,Java 刷题小程序
,PMP 刷题小程序
,点击个人公众号菜单打开!
另外有 111 本架构师资料以及 1000 道 Java 面试题,都整理成了PDF。
能够关注公众号 「悟空聊架构」 回复 悟空
领取优质资料。
「转发->在看->点赞->收藏->评论!!!」 是对我最大的支持!
《Java并发必知必会》系列:
1.反制面试官 | 14张原理图 | 不再怕被问 volatile!