Java中解决CAS机制出现的ABA问题?
1、先了解一下什么是CAS?
一句话总结就是: 比较并交换(compare and swap)是一条CPU并发原语
CAS的公式如下:
CAS(V,A,B)
1:V 表示内存中的地址
2:A 表示预期值
3:B 表示要修改的新值
CAS的功能:就是预期值A与内存中的值相比较,如果相同则将内存中的值改变成新值B
2、CAS的底层原理?
换句话说也就是CAS为什么能保证原子性?
(1)靠的是底层的Unsafe类
(2)Unsafe类是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。 Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的方法
(3)注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底昃资源执行相应任务,Unsafe类中的native方法是调用底层原语,原语是有原子性的
3、CAS的问题?
(1)循环时间长,开销比较大
CAS是Java乐观锁的一种实现机制,在Java并发包中,大部分类就是通过CAS机制实现的线程安全,它不会阻塞线程,如果更改失败则可以自旋重试,允许多线程并发修改,但是互相比较,互相比较以后直到全部的线程执行成功,并发性加强了,但是循环时间长,开销大。
解决办法:JVM支持处理器提供的pause指令,使得效率会有一定的提升,pause指令有两个作用:
(1)第一它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源
(2)第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)
而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
(2)只能保证一个共享变量的原子操作
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
解决办法:从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,
你可以把多个变量放在一个对象里来进行CAS操作。
(3)ABA问题
形象的说就是,狸猫换太子
ABA问题就是 讲桌上面放了1瓶水,张三有10秒的操作时间,他先把水换成了 水果,用了2秒,接着他又把水果换成了水,尽管最后的结果没有发生改变,但是这之间有很多次的操作机会,所以就造成了漏洞,也就是常说的狸猫换太子(比喻:你老婆出轨之后又回来,还是原来的老婆吗?)
4、怎么解决ABA问题?
AtomicReference原子引用
如果赋值操作不是线程安全的。若想不用锁来实现,可以用**AtomicReference**这个类,实现对象引用的原子更新
两种解决方法:
(1)AtomicStampedReference 类:
版本号原子引用,理解原子引用+新增一种机制,那就是修改版本号(类似时间戳)
(2)AtomicMarkableReference 类
(1)AtomicStampedReference 示例:
看示例代码之前,请先去看看AtomicStampedReference 方法的API可以帮助理解
链接: 点击这里查看方法各个参数的含义.
示例:
package com.song.test01;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test09 {
public static void main(String[] args) {
String str1 = "aaa";
String str2 = "bbb";
//1、传入初始引用 和 初始标志 :aaa 和 1
AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1,1);
System.out.println("reference.getReference() = " + reference.getReference()+",版本号为"+reference.getStamp());
System.out.println("============================================");
/*
2、(1)第一步 判断 当前引用(也就是初始引用str1) 和 该引用的预期值str1是否相等 str1 == str1 所以相等
(2)第二步 判断 当前标志(也就是上面的版本号1)和 预期标志reference.getStamp() 是否相等
(3)如果上面两步都相等了,则以原子方式将该引用和该标志的值设置为给定的更新值
(4)str2是新的值,reference.getStamp() + 1 是新的版本号
*/
boolean b1 = reference.compareAndSet(str1, str2, reference.getStamp(), reference.getStamp() + 1);
System.out.println("b1:"+b1);
System.out.println("reference.getReference() = " + reference.getReference()+",版本号为"+reference.getStamp());
System.out.println("===========================================");
/* 3、 (1)如果当前引用 == 预期引用(str2),则以原子方式将该标志的值设置为给定的更新值
(2)该标志的新值 reference.getStamp() + 1
*/
boolean b2 = reference.attemptStamp(str2, reference.getStamp() + 1);
System.out.println("b2: "+b2);
//这里把版本号 改为 3
System.out.println("reference.getStamp() = "+reference.getStamp());
System.out.println("==========================================");
/* 4、(1)和2步骤中基本像素
如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值
(2)这里虽然 当前引用(bbb)和预期引用(str2)相等,但是版本号不一致,当前版本号是3,预期版本号是4
*/
boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1);
System.out.println("reference.getReference() = " + reference.getReference()+",版本号为"+reference.getStamp());
System.out.println("c = " + c);
}
}
输出结果:
通过stamp这个标记(版本号)属性来记录CAS每次设置值的操作,而下一次再CAS操作时,由于期望的stamp与现有的stamp不一样,因此就会设值失败,从而杜绝了ABA问题的复现
(2)AtomicMarkableReference
基本和AtomicStampedReference差不多,AtomicStampedReference主要关注版本号,即reference的值被修改了多少次;AtomicMarkableReference是使用boolean mark来标记reference是否被修改过
既然有了 AtomicStampedReference 为啥还需要再提供 AtomicMarkableReference 呢,在现实业务场景中,不关心引用变量被修改了几次,只是单纯的关心是否更改过。
查看示例前可以看看 API,方便理解各个参数
链接: 点击这里查看方法的各个参数含义.
示例:
package com.song.test01;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class Test10 {
/**
* initialRef- 初始参考 - cat
* initialMark- 初始标记 - false
*/
static AtomicMarkableReference<String> atomicStampedReference = new AtomicMarkableReference("cat",false);
public static void main(String[] args) {
// public boolean isMarked() 返回标记的当前值。
boolean oldMarked = atomicStampedReference.isMarked();
//public V getReference() 返回参考的当前值。
String oldReference = atomicStampedReference.getReference();
System.out.println("初始化之后的标记:"+oldMarked);
System.out.println("初始化之后的值:"+oldReference);
System.out.println("==============================================");
String newReference = "dog";
boolean b =atomicStampedReference.compareAndSet(oldReference,newReference,true,false);
if(!b){
System.out.println("Mark不一致,无法修改Reference的值");
}
b =atomicStampedReference.compareAndSet(oldReference,newReference,false,true);
if(b){
System.out.println("Mark一致,修改reference的值为dog");
}
System.out.println("修改成功之后的Mark:"+atomicStampedReference.isMarked());
System.out.println("修改成功之后的值:"+atomicStampedReference.getReference());
}
}