面试官问你:知道什么是ABA问题吗?

大家好啊,我是你们的老朋友青戈,我又来分享干货啦😉最近有没有好好学习啊?我不在的时候也不能懈怠哦~毕竟升职加薪还得靠自己努力的呀😆

在开始问题阐述之前呢,我们先看一则小故事:

狸猫换太子

在开始问题的阐述之前,我们先来看一则小故事:

北宋宋真宗皇后死后,当时他的两位爱妃刘妃和李妃都怀了孕,很显然,谁生了儿子,谁就有可能立为正宫。刘妃久怀嫉妒之心,唯恐李妃生了儿子被立为皇后,于是与宫中总管都堂郭槐定计,在接生婆尤氏的配合下,乘李妃分娩时由于血晕而人事不知之机,将一狸猫剥去皮毛,血淋淋,光油油地换走了刚出世的太子。刘妃命宫女寇珠勒死太子,寇珠于心不忍,暗中将太子交付宦官陈琳,陈琳将太子装在提盒中送至八贤王处抚养。再说真宗看到被剥了皮的狸猫,以为李妃产下了一个妖物,乃将其贬入冷宫。不久,刘妃临产,生了个儿子,被立为太子,刘妃也被册立为皇后。谁知六年后,刘后之子病夭。真宗再无子嗣,就将其皇兄八贤王之子(实为当年被换走的皇子)收为义子,并立为太子。

从这故事看出来,太子出生被被换成了狸猫,最后阴差阳错又回归成为太子。虽然结果是一样的,但是过程曲折了,太子真的是命途多舛啊。

为什么说这个故事?其实跟我们今天要介绍的问题有很大的关系。同样的结果,可能中间不知道发生了多少次操作,那么我们能认为他没变吗?在不同的业务场景下,我们要仔细考虑这个问题。

ABA问题描述

在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下:

  1. 线程1,期望值为A,欲更新的值为B
  2. 线程2,期望值为A,欲更新的值为B

线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。

举个具体的例子说明

小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50:

  • 线程1(提款机):获取当前值100,期望更新为50;
  • 线程2(提款机):获取当前值100,期望更新为50;
  • 线程1成功执行,线程2某种原因block了;
  • 这时,某人给小明汇款50;
  • 线程3(默认):获取当前值50,期望更新为100,这时候线程3成功执行,余额变为100;
  • 线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50。

此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来错误提交结果。

解决方法

要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1。

代码示例

通过AtomicStampedReference来解决ABA问题

  • AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号;

  • 当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。

private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);

public static void main(String[] args) {
//第一个线程
	new Thread(() -> {
		System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());
		
		//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
		atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
	},"t1").start();
	
  // 第二个线程
	new Thread(() -> {
		int stamp = atomicStampedReference.getStamp();
		System.out.println("t2拿到的初始版本号:" + stamp);
		
		//睡眠3秒,是为了让t1线程完成ABA操作
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("最新版本号:" + atomicStampedReference.getStamp());
		System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前值:" + atomicStampedReference.getReference());
	},"t2").start();
}

1、初始值100,初始版本号1

2、线程t1和t2拿到一样的初始版本号

3、线程t1完成ABA操作,版本号递增到3

4、线程t2完成CAS操作,最新版本号已经变成3,跟线程t2之前拿到的版本号1不相等,操作失败

执行结果:

t1拿到的初始版本号:1
t2拿到的初始版本号:1
最新版本号:3
false 当前值:100

通过AtomicMarkableReference解决ABA问题

AtomicStampedReference可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D -> A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次。

但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
AtomicMarkableReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。

private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(100,false);

public static void main(String[] args) {
// 第一个线程
	new Thread(() -> {
		System.out.println("t1版本号是否被更改:" + atomicMarkableReference.isMarked());
		
		//睡眠1秒,是为了让t2线程也拿到同样的初始版本号
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		atomicMarkableReference.compareAndSet(100, 101,atomicMarkableReference.isMarked(),true);
		atomicMarkableReference.compareAndSet(101, 100,atomicMarkableReference.isMarked(),true);
	},"t1").start();
	
  // 第二个线程
	new Thread(() -> {
		boolean isMarked = atomicMarkableReference.isMarked();
		System.out.println("t2版本号是否被更改:" + isMarked);
		
		//睡眠3秒,是为了让t1线程完成ABA操作
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("是否更改过:" + atomicMarkableReference.isMarked());
		System.out.println(atomicMarkableReference.compareAndSet(100, 2019,isMarked,true) + "\t当前值:" + atomicMarkableReference.getReference());
	},"t2").start();
}

1、初始值100,初始版本号未被修改 false

2、线程t1和t2拿到一样的初始版本号都未被修改 false

3、线程t1完成ABA操作,版本号被修改 true

4、线程t2完成CAS操作,版本号已经变成true,跟线程t2之前拿到的版本号false不相等,操作失败

执行结果:

t1版本号是否被更改:false
t2版本号是否被更改:false
是否更改过:true
false 当前值:100

多说几句

以上是本期关于CAS领域的一个经典ABA问题的解析,不知道你在实际的工作中有没有遇到过,但是在面试中这块是并发知识考查的重点。如果你还没接触过此类的问题,我的建议是你自己将上面的代码运行一下,结合理论去理解一下ABA问题所带来的问题以及如何解决他,这对你日后的开发工作也是有莫大的帮助的!

我是程序员青戈,本期的面试问题分享到这里就结束了,想提升自己,进阶大厂的同学一定要关注我的公众号:Java学习指南,在这里每天会从实际的面试出发带你学习和总结Java相关的知识,帮助你扩充技术栈,提升个人实力。我们下期见~

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员青戈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值