目录
- 1、概念
- 2、原理
- 3、缺点
- 4、ABA问题
- 5、解决ABA问题
1、概念
CAS(Compare And Swap): 比较并替换,它是一条CPU原语,是一条原子指令(原子性)。
CAS通过比较真实值与预期值是否相同,如果是则进行修改,Atomic原子类底层就是使用了CAS,CAS属于乐观锁。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么会自动将该内存位置的值更新为新值,反之不做任何操作。
2、原理
CAS执行依赖于Unsafe类,Unsafe类的所有方法通过native修饰,所以Unsafe类直接操作系统底层的数据地址,而不通过JVM实现。
比如:A、B线程通过AtomicInteger同时对变量进行自增
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 使用了unsafe类
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获取原值
var5 = this.getIntVolatile(var1, var2);
// 将原值与预期值进行对比,一致则进行相加var4
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
伪代码流程解释:
A线程将变量num从1进行自增,当执行时没有其他线程修改过num值,所以一次成功,num自增后将值返回给主内存。
B线程也将num进行自增,但是num已经被A线程进行了修改,所有将再次执行do-while,重新获取num的值。
由于JMM模型的可见性,B线程重新获取值时会从主内存中获取到被A修改后的最新值。
通过最新值与期望值进行比较,满足条件就返回true。
CAS如何实现:
通过Unsafe类对系统底层的数据地址进行原子性操作,对比内存地址的值和预期值是否一样,如果一样进行修改。
3、缺点
- 1、循环时间长,增加了CPU的开销。
- 2、只能保证一个变量的原子操作。
- 3、会导致ABA问题。
4、ABA问题
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的,因为内存X中的值从A到B再到了A。
CAS执行时,将过去某时刻的值与当下时刻进行比较并替换,在这时间差中,值可能会发生多次修改,只是最终值的结果不变。
5、解决ABA问题
为了防止在值比较时,存在被修改过的可能,通过为值加上版本号的方式,在最后执行CAS时判断版本号,确保不会出现ABA问题。
通过原子引用(AtomicReference)加时间戳原子引用(AtomicStampedReference)解决ABA问题。
代码如下:
public class Test {
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=========以下ABA问题产生:=========");
// 线程 t1 模拟ABA问题的产生
new Thread(() -> {
// 进行一次修改,将值修改为101
atomicReference.compareAndSet(100, 101);
// 进行第二次修改,将值修改回100
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
// 线程t2暂停1S,保证线程t1执行完成
try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
System.out.println(atomicReference.compareAndSet(100, 102));
System.out.println("修改成功,修改后值为:"+atomicReference.get());
}, "t2").start();
try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}
System.out.println("=========以下ABA问题解决方式:=========");
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("线程名称:"+Thread.currentThread().getName()+",第一次版本号:"+stamp);
try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
// 进行一次修改,将值修改为101
atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
stamp = atomicStampedReference.getStamp();
System.out.println("线程名称:"+Thread.currentThread().getName()+",第二次版本号:"+stamp);
// 进行第二次修改,将值修改回100
atomicStampedReference.compareAndSet(101,100,stamp,stamp+1);
stamp = atomicStampedReference.getStamp();
System.out.println("线程名称:"+Thread.currentThread().getName()+",第三次版本号:"+stamp);
}, "t3").start();
new Thread(() -> {
// 获取版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("线程名称:"+Thread.currentThread().getName()+",第一次版本号:"+stamp);
// 线程t4暂停1S,保证线程t3执行完成
try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}
boolean andSet = atomicStampedReference.compareAndSet(100, 101, stamp,stamp+1);
System.out.println("线程名称:"+Thread.currentThread().getName()+",修改结果:"+andSet+",第二次版本号:"+stamp+1+"实际版本号:"+atomicStampedReference.getStamp());
System.out.println("当前最新值:"+atomicStampedReference.getReference());
}, "t4").start();
}
}
分析:
在线程t1
中,将值从100修改为了101又修改回了100,但是线程t2
却成功更新了值,所以产生了ABA问题。
在线程t3
中,将值从100修改为了101又修改回了100,并且更新的版本号为3;在线程t4
中,将100修改为101时,由于线程t4
更新时的版本号为2,但是实际的版本号为3,所以无法更新,解决了ABA问题。
运行结果: