java cas 原理_Java进阶:CAS原理详解

概述

CAS是Compare And Swap的简称,即:比较并交换

Java中常见的是Atomic相关类使用了CAS,例如:AtomicInteger、AtomicBoolean等等

实现CAS的底层用的是Unsafe操作类

先看个小例子

public static void main(String[] args) {

AtomicInteger atomicInteger = new AtomicInteger(5);

atomicInteger.compareAndSet(5, 100);

System.out.println(atomicInteger.get());

atomicInteger.compareAndSet(5, 200);

System.out.println(atomicInteger.get());

}

输出:

100

100

compareAndSet(int expect, int update):将atomicInteger数值与expect比较,相等就把update赋给atomicInteger,否则什么都不做。

所以,上面的例子只有第一次是修改成功的,第二次修改失败,值仍然为100.

compareAndSet 源码:调用了unsafe.compareAndSwapInt

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

this:当前AtomicInteger对象

valueOffset:AtomicInteger的value字段在内存中的偏移量

expect:期望值,需要比较的值

update:如果value==expect,需要更新替换(Swap)的数据

Unsafe

Unsae是CAS的核心类,由于Java无法直接访问底层系统,需要通过本地native方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

Unsafe类存在于sun.misc包中,其内部方法可以像C语言的指针一样直接操作内存,因此Java中CAS的操作执行依赖于Unsafe类的方法。

现在,再来看看上面的compareAndSet,内部使用的unsafe.compareAndSwapInt,其实底层就一句native代码。但是,最终执行是由一系列原子的、不可穿插、不可中断的指令组成,以此来实现CAS原子性操作

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

例子2:getAndIncrement

AtomicInteger还有个常用的方法:getAndIncrement。作用就是把当前数值+1,跟i++效果一样。但是getAndIncrement是线程安全的,而i++不是线程安全。

getAndIncrement 源码解析,调用的是 unsafe.getAndAddInt

public final int getAndIncrement() {

return unsafe.getAndAddInt(this, valueOffset, 1);

}

再来看下 unsafe.getAndAddInt 源码

public final int getAndAddInt(Object var1, long var2, int var4) {

int var5;

do {

var5 = this.getIntVolatile(var1, var2);

} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;

}

根据 AtomicInteger 的 valueOffset,即内存偏移量,获取内存中的值 var5

执行 compareAndSwapInt,把 var5 再次和内存的值比较一次,相同就执行 var5 + var4(这里var4=1)

如果上一步比较结果不同,则重新回到第1步,重新执行

返回 var5

CAS缺点

getAndAddInt使用的是do…while,执行失败会一直反复尝试。如果长时间一直不成功,会给CPU带来较大的开销。

只能保证一个共享变量的原子操作,如果需要保证多个变量的原子操作,可以采用锁,或者AtomicReference操作对象

引出来ABA问题

ABA问题

ABA 问题阐述

假设主内存有变量A=0,现在2个线程都准备执行compareAndSet(0, 10)

由于线程2的执行速度较快,把A改成10,然后又改成了20,最后又改成10

此时线程1也正准备执行,因为主内存A=10,线程1中也是A=10,就以为当前变量A没人操作过,所以线程1的compareAndSet也执行成功了

表面上看似乎也没什么问题,但是这个现象,在特定的业务条件下可能就会导致Bug

ABA 问题解决

使用AtomicStampedReference可以解决ABA问题,因为这个类携带版本号功能。除了比较当前值和预期值,还会比较当前版本号和预期版本号。如果版本号不一致,则同样更新失败。

public class AtomicStampedReferenceApp {

public static void main(String[] args) {

//初始化AtomicStampedReference对象,值为5,版本号为1

AtomicStampedReference atomic = new AtomicStampedReference(5, 1);

new Thread(() -> {

int stamp = atomic.getStamp();

System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);

//Sleep 1s

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

//先从5改成10

atomic.compareAndSet(5, 10, atomic.getStamp(), atomic.getStamp()+1);

//再从10改成5

atomic.compareAndSet(10, 5, atomic.getStamp(), atomic.getStamp()+1);

//输出结果,这时值为5,版本号为3

System.out.println(Thread.currentThread().getName() + "当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());

}, "thread-1").start();

new Thread(() -> {

int stamp = atomic.getStamp();

System.out.println(Thread.currentThread().getName() + "初始版本号:" + stamp);

//Sleep 3s,保证thread-1先执行完毕,到时版本号会更高

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

//预期将值从5改成100,预期版本号是1

boolean result = atomic.compareAndSet(5, 100, stamp, stamp+1);

System.out.println(Thread.currentThread().getName() + "修改成功:" + result + ",当前值:" + atomic.getReference() + ",当前版本号:" + atomic.getStamp());

}, "thread-2").start();

}

}

结果如下,thread-2修改失败,因为当前版本号已经变成3了,比预期的1还高

thread-1初始版本号:1

thread-2初始版本号:1

thread-1当前值:5,当前版本号:3

thread-2修改成功:false,当前值:5,当前版本号:3

你可能感兴趣的:(java,cas,原理)