在我们平时开发工作中,只要遇到并发相关的,首先会想到原子操作,哦,不对是道格·利,紧接着是AtomicInteger,AtomicLong,LongAccumulator,LongAdder等一堆原子类直冲天灵盖,hold down~~ hold down~~ ,我们今天不讲这些,今天我们讲讲他们的父亲,哦,不,是二大爷CAS.
什么是CAS
CAS(Compare And Swap 比较并交换),CAS本身是一种思想,一种无锁算法,可以看做是乐观锁的一种实现方式。
通俗解释:我要修改一个值V,我期望V的值是6,如果我从内存取出来的值确实为6,与我期望的一致,那我就把他修改为8;如果取出来的不是6,那我就认为他被别人修改过,与我期望的不一致,那我就放弃对他的修改。
从上边的例子我们可以看出CAS有三个操作数:
- 内存值V
- 预期值6
- 要修改的值8
当且仅当预期值6与内存值V相同时,才会将内存值修改为8,否则什么都不做,并放回现在的值V
下面我们通过一段代码再来加深一下对这个概念的理解:
package com.yqj.juc.jmm;
/**
* CAS 原理演示
* @author Zhao Yun Long
* @version V1.0
* @date 2022/11/02 15:06
*/public class CasDemo implements Runnable {
private volatile int value;
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue){
value = newValue;
System.out.println("线程" + Thread.currentThread().getName() + "修改成功");
}else {
System.out.println("线程" + Thread.currentThread().getName() + "修改失败");
}
return oldValue;
}
public static void main(String[] args) throws InterruptedException {
CasDemo casDemo = new CasDemo();
casDemo.value = 0;
Thread t1 = new Thread(casDemo,"t1");
Thread t2 = new Thread(casDemo,"t2");
t1.start();
t2.start();
t1.join();
t1.join();
}
@Override
public void run() {
compareAndSwap(0,1);
}
}
运行结果第一个线程修改成功后,第二个线程compare为false,修改失败。
CAS应用场景
其实在JUC包下很多原子类(我们开篇提到了部分)都是通过CAS来实现的,我们就以AtomicInteger为例来看下:
package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
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); }
}
private volatile int value;
/**
* 省略若干代码。。。。。。。。。。
*/
public final boolean compareAndSet(int expect, int update) {
//来,看这儿---------@-↓↓↓↓↓↓↓-@---------
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
//来,看这儿---------@-↑↑↑↑↑↑↑-@---------
}
/**
* 省略若干代码。。。。。。。。。。
*/
}
我们能看到compareAndSet(int expect, int update) 方法调用了unsafe类中的compareAndSwapInt(this, valueOffset, expect, update),这一步暂时到这儿,忽略compareAndSwapInt方法的前两个参数, expect, update这两个参数我们看名字也能猜个大概,expect对应的就是上边我们自己实现的CasDemo类中的expectedValue,update对应的就是newValue。到这一步也验证了AtomicInteger是通过CAS实现的,我们再继续往深了走,方便我们更深的理解CAS.
CAS源码分析
上边我们跟到了unsafe.compareAndSwapInt(this, valueOffset, expect, update),我们继续点进去可以看到一下代码:
我们可以看到有三个类似的方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了以上三种针对不同类型变量的 CAS 操作。
从图中我们也能看出,Unsafe类最终调用的是native方法,底层其实是JVM通过C++调用处理器的指令cmpxchg来实现的。这里我们以linux_64x的为例,查看Atomic::cmpxchg的实现:
#atomic_linux_x86.inline.hpp
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 架构中的一个原子条件指令,
//它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
//如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。
//这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
__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;
源码看不懂没关系,我们只需要知道CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。
那人无完人,同样CAS也是有天生缺陷的,他虽然高效地解决了原子操作,但是还是存在一些缺陷的:
- 自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
- 只能保证一个共享变量原子操作
- ABA 问题
什么是ABA问题
假设我们原来的值为6,通过操作更新成了8,经过其他业务处理后又变成了6,这时候我们CAS去检查的时候发现值是6,他会认为这个值没变过,然后就把他更新了,但其实这个值已经被变过了。 针对这个问题,我们可以增加版本号,每次修改的时候,版本号累加,就类似数据库中的乐观锁。 同样,Java也提供了相应的原子引用类AtomicStampedReference:
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。
关于CAS的突击就到这儿啦,文章有错误的地方请评论区指正。