CAS
在说CAS之前我们先看一个小例子:多线程下计数器的问题
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
//创建10个线程每个线程循环1000次,正常的话计算出来的sum应该是10000
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 1000; i1++) {
num++;
}
}
});
thread.start();
}
Thread.sleep(1000);
System.out.println(num);
}
但是运行之后会发现num的值不固定例如:9627、9297等,那有什么方案可以实现最终结果是10000,那有的同学说可以使用synchronized 和加锁lock。
但是上面两个方案都会导致线程阻塞挂起,进而导致线程上下文切换和重新调度的开销,那有没有不加锁的方案也能实现呢?
CAS也是可以实现上面的要求,可以考虑使用AtomicInteger,当然自己也可以使用Unsafe实现简化版的AtomicInteger,Atomic这些底层也是通过CAS来实现的。那我们来看一下什么是CAS。
什么是CAS
CAS(Compare and Swap,比较并交换),通常如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect的一种非阻塞原子操作。java中通过UnSafe里面的compareAndSwapObject、compareAndSwapInt、compareAndSwapLong实现的。
CAS操作有4个参数
- 对象内存中位置
- 对象中变量的偏移量
- 变量预期值
- 新的值
CAS简单使用
public class CasSimple {
private static Entry entry = new Entry();
public static void main(String[] args) throws Exception {
Unsafe unsafe = getUnsafe();
long valueOffset = unsafe.objectFieldOffset(entry.getClass().getDeclaredField("num"));
System.out.println(valueOffset);
boolean result = unsafe.compareAndSwapInt(entry, valueOffset, 0, 1);
System.out.println("第一次cas操作结果:" + result +", num = " + entry.num);
result = unsafe.compareAndSwapInt(entry, valueOffset, 1, 3);
System.out.println("第二次cas操作结果:" + result +", num = " + entry.num);
result = unsafe.compareAndSwapInt(entry, valueOffset, 1, 5);
System.out.println("第三次cas操作结果:" + result +", num = " + entry.num);
}
private static Unsafe getUnsafe() throws Exception{
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(Unsafe.class);
}
}
class Entry{
int num = 0;
}
运行结果如下:
第一次cas操作结果:true, num = 1
第二次cas操作结果:true, num = 3
第三次cas操作结果:false, num = 3
CAS实现原理
由于是native方法,我们看一下jdk的源码看看是怎么实现的,这看的是openJDK的代码,找到unsafe.cpp文件
// offset 偏移量、e要比较的值、x期望设置的值
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
//根据偏移量计算出value的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//CAS操作逻辑
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
核心逻辑在Atomic::cmpxchg方法中,这个是根据不同操作系统和不同的CPU有不同的试下,这里查看的是linux_x86中的实现文件在 atomic_linux_x86.inline.hpp
exchange_value最终要替换的值, dest表示某值对应的内存地址,compare_value要比较的值,
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
//判断当前执行环境是否为多处理器环境
int mp = os::is_MP();
//LOCK_IF_MP(%4)在多处理器环境下,为cmpxchgl指令添加lock前缀,以达到内存屏障的效果
//cmpxchgl 是一种包含在x86架构及IA-64架构中的一个原子操作的汇编指令
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序从输出开始从左往右 以 "%0"开始,
所以cmpxchgl %1,(%3) 实际上表示cmpxchgl exchange_value,(dest),cmpxchgl中有一个隐含操作数eax,实际过程是先比较eax对应的值(也就是compare_value)和dest进行比较, CAS指令作为一种硬件原语,有着天然的原子性
#第一个输出参数,把eax中的值写入到exchange_value中,方法结束后返回
%0:"=a" (exchange_value)
#表示compare_value存入eax寄存器、exchange_value、dest、mp存入任意的通用寄存器
%1:"r" (exchange_value),
%2:"a" (compare_value),
%3:"r" (dest),
%4:"r" (mp)
执行过程:
cmpxchgl执行时先判断cmpare_value和dest是否相等,如果相等则把eax对应的值指向exchange_value,把eax存的compare_value 赋值给echange_value, 这个时候实际返回的是compare_value对应的地址。 如果compare_value 和 dest不相等,则把dest的值写入到eax,然后在赋值给exchange_value进行返回
伪代码如下:
a = 10;//cmpare_value
b = 10;//dest
c = 12;//exchange_value
eax = a;
if(a == b){
eax = c;
c = a;
} else {
eax = b;
c = eax;
}
return c;
经过上面的分析大家是不是已经对CAS怎么使用和工作原理已经清楚了呢,那我们来总结一下CAS的优缺点
CAS 优缺点
优点
可以高效的解决原子操作
缺点
-
自旋CAS长时间不成功则会给CPU带来非常大的开销
java中原子类递增都是通过CAS自旋实现的,如下代码:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); //如果长时间自旋失败则会给CPU带来较大的开销 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } -
只能保证一个共享变量的原子操作
就拿AtomicInteger举例,一次只能保证一个共享变量的原子性
-
ABA问题
说明一下第三个ABA缺陷,那什么是ABA问题呢?三个线程 A线程设置变量为10,B线程把变量改为20,C又把变量改回10,这个时候A去修改变量的时候还是成功的,但是实际上值已经有被改动过了如下图:
通过代码验证一下
private static AtomicInteger atomicInteger = new AtomicInteger(10);
public static void main(String[] args) throws Exception {
new Thread(() -> {
int value = atomicInteger.get();
System.out.println("ThreadA get value = " + value);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(atomicInteger.compareAndSet(value, 20)){
System.out.println("ThreadA update "+value +" to 20");
}else {
System.out.println("ThreadA update fail!");
}
},"ThreadA").start();
new Thread(() -> {
int value = atomicInteger.get();
System.out.println("ThreadB get value = " + value);
if(atomicInteger.compareAndSet(value, 20)){
System.out.println("ThreadB update "+value +" to 20");
}
value = atomicInteger.get();
System.out.println("ThreadB get value = " + value);
if(atomicInteger.compareAndSet(value, 10)){
System.out.println("ThreadB update "+value +" to 10");
}
},"ThreadB").start();
}
运行结果如下:
ThreadA get value = 10
ThreadB get value = 10
ThreadB update 10 to 20
ThreadB get value:20
ThreadB update 20 to 10
ThreadA update 10 to 20
可以看出 ThreadA 通过CAS操作还是可以设置成功的。
但是有的业务场景下ABA这种问题是要解决的。那怎么解决呢?可以使用java提供的AtomicStampedReference,看如下例子
//第一个变量为实际存储的变量,第二个变量为版本信息 每次修改则会+1
private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference(10, 1);
public static void main(String[] args) throws Exception {
new Thread(() -> {
int[]stampHolder = new int[1];
int value = atomicInteger.get(stampHolder);
//可以使用stampHolder[0]表示版本,主要是因为 get()方法有这一句话 stampHolder[0] = pair.stamp;
System.out.println("ThreadA get value = " + value+" 版本为 = " + stampHolder[0]);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(atomicInteger.compareAndSet(value, 20,stampHolder[0],stampHolder[0]+1)){
System.out.println("ThreadA update "+value +" to 20");
}else {
System.out.println("ThreadA update fail!");
}
},"ThreadA").start();
new Thread(() -> {
int[]stampHolder = new int[1];
int value = atomicInteger.get(stampHolder);
System.out.println("ThreadB get value = " + value+" 版本为 = " + stampHolder[0]);
if(atomicInteger.compareAndSet(value, 20,stampHolder[0], stampHolder[0]+1)){
System.out.println("ThreadB update "+value +" to 20");
}
value = atomicInteger.get(new int[1]);
System.out.println("ThreadB get value = " + value+" 版本为 = " + stampHolder[0]);
if(atomicInteger.compareAndSet(value, 10,stampHolder[0], stampHolder[0]+1)){
System.out.println("ThreadB update "+value +" to 10");
}
},"ThreadB").start();
}
运行效果如下:
ThreadA get value = 10 版本为 = 1
ThreadB get value = 10 版本为 = 1
ThreadB update 10 to 20
ThreadB get value = 20 版本为 = 1
ThreadA update fail!
在使用过程中根据自己的场景选择相应的Atomic原子操作类。上面介绍了AtomicInteger 和AtomicStampedReference 那都有哪些Atomic原子操作类呢,下面跟着胖哥一块来看看吧。
Atomic原子操作
java使用CAS在JUC包下的atoimic包中提供了一些列的原子操作类
- 基本类型:AtomicInteger、AtomicLong、AtomicBoolean
- 引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference
- 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
- 原子类型累加器(1.8新增的) :DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64
这里就简单描述一下AtomicInteger,LongAdder下一篇在单独分析。
AtomicInteger
那我们接下来看看AtomicInteger的属性和get、set方法
//Unsafe类上面已经说过,里面包含了几个CAS相关的native方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
//偏移量
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//对应的值 加上volatile为了保证可见性
private volatile int value;
来看一下 getAndIncrement方法 自增方法,每次+1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe中的方法,
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//这个是native方法
var5 = this.getIntVolatile(var1, var2);
//执行CAS操作,如果失败则进行自旋操作 ①
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
考虑一下在高并发环境下 上面标注的①代码 是否有性能瓶颈呢?
在JDK1.8中引入了LongAdder其目的就是解决高并发环境下AtomicInteger, AtomicLong的自旋瓶颈问题。胖哥下一篇再来分析下LongAdder解决自旋瓶颈问题的。
总结
通过上面的例子和源码分析大家已经对CAS有一定的了解了,如果遇到相关的场景了就赶快用起来吧。
我是二胖,公众号:二胖学技术