CAS
CAS是Java中的一种并发控制技术,全称为Compare And Set\Swap(比较与交换)。
其主要思想是在没有锁的情况下,使用CPU的原子性指令(cmpxchg指令),通过硬件保证了比较-更新的原子性,比较共享变量的值与预期的值是否相等,如果相等则更新此变量的值,否则不做任何操作。而在此过程中,涉及到了三个值,位置内存值(主内存的值),旧的预期值(工作内存从主内存加载的值),更新值,如果位置内存值和旧的预期值不相同,那么本次更新值作废。
CAS操作可以保证并发安全,避免了使用锁带来的性能损失,常被用于性能要求高的并发编程场景中。Java提供了Atomic类中的compareAndSet()方法来实现CAS操作。
原子类
在 java.util.concurrent.atomic包下的类,都是原子类。
所谓原子类是一组基于CAS操作实现的线程安全的工具类,它提供了一套原子性的操作,用来实现线程安全的并发编程。Java提供的原子类包括AtomicBoolean、AtomicInteger、AtomicLong等,它们支持原子性地进行基本数据类型的读取、赋值、加减、比较等操作。
使用原子类的好处就是,它不同于 synchronized重量级锁,它是轻量级的操作,比之原子性更好,更有效。
原子类的实现原理是基于硬件和JVM底层的CAS指令:
- 首先,Atomic类会使用CAS操作读取并更新共享变量的值,当共享变量的值与预期值相同时,CAS指令会将新的值更新到共享变量中并返回true,否则CAS指令返回false;
- 其次,在多线程并发访问共享变量时,CAS指令可以保证只有一个线程可以成功更新共享变量,如果多个线程同时执行CAS指令,则只有一个线程会成功更新共享变量的值。
AtomicInteger a = new AtomicInteger();
public void getAtomicInteger(){
int b = a.get();
a.set(b++);
}
//当线程调用这个方法的时候,这个变量就能保证原子性,
//并且类的内部属性是private volatile int value;同时兼具可见性和禁重排
其中关于CAS的方法:compareAndSet(int expect,int update)
参数一为期望值,二为更新值。
在底层源码中调用的是Unsafe对象的方法compareAndSwapInt(this,valueOffset,expect,update)
是一个本地方法。
也就是说,它实现CAS的原理其实是靠Unsafe类。
实现原理
我们需要通过Unsafe这个类来了解CAS的实现原理。
首先我们对AtomicInteger作为例子进行介绍。
AtomicInteger
在AtomicInteger类中有三个属性,分别是:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
其中value用于记录变量的值,valueOffset记录内存偏移地址(表示对象某个字段在该类内存的偏移量,在这个类中是value属性的偏移地址),unsafe用于调用本地方法。
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
因为这里获取的字段是value,所以记录的是它的偏移地址。
然后通过unsafe调用本地方法从硬件上直接对内存进行计算。
在底层使用CAS的方法主要有两种:
- getAndSetXxx(xxxx)
- getAndAddXxx(xxxx)
现在我们就getAndAddInt举例说明
getAndAddInt
在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调用它的时候会传入三个参数:unsafe.getAndAddInt(this, valueOffset, 1);
回到源码中,它使用var5记录this+valueOffset的值,然后在while判断条件中执行比较:
public final native boolean compareAndSwapInt
(Object var1, long var2, int var4, int var5);
该方法具有以下参数:
var1:需要进行操作的对象。var2:需要进行操作的对象中某个字段的偏移量。var4:期望的值。var5:新的值。
该方法的返回值为一个布尔型,表示操作是否成功。如果原始值与期望值相等(这里再次利用var1+var2的地址取值和var5作比较),则将字段的值更新为新的值(var5+var4),并返回 true退出循环,否则不更新(写入var5,称之为自旋)并返回 false继续循环。
这个方法在Unsafe.cpp中调用了一个Atomic::cmpxchg(xxx,xxx,xxx)的函数。
这里调用的是CPU指令,它的参数都是指令的操作码,于是没有方法体,并且会根据不同的操作系统调用重载的函数。
从这里也可以看出,CAS是通过底层指令实现的,所以容易内存管理出错,在JDK9之后就不建议使用了,如果你是面向硬件的工程师,可以深入学习。
并且可以知道,因为它是一个CPU指令,那么它必定有原子性,是不可中断的操作。
ps:AtomicReference它是一个泛型类,用于扩展AtomicInteger等基本原子类
pps:我们可以利用API方法手动写自旋锁。
CAS缺点
循环时间长、开销大
在轮询期望值的时候,如果一直失败,就一直轮询,这样造成CPU消费。
ABA问题
如果线程1在内存位置取出A,线程2同时在相同位置取出A,并改为B又改为A,然后线程1轮询发现和期望值A一样是A,那么线程1退出轮询。
尽管线程1成功,但是不代表是正确的。
于是诞生了一个解决方案:版本号。在Java中使用了一个API实现:AtomicStampedReference
public class test{
public static void main(String[] Args){
Integer number = 1;
AtomicStampedReference<Integer> stamped = new AtomicStampedReference<>(number,1);
System.out.println(stamped.getReference()+stamped.getStamp());
Integer number2 = 2;
stamped.compareAndSet(number,number2,stamped.getStamp(),stamped.getStamp()+1);
//这里传入期望值和更新值,期望版本号和更新版本。
System.out.println(stamped.getReference()+stamped.getStamp());
}
}
总结
在本节,我们学习了CAS及其基本原理,它使用于多线程情况下,对共享变量的读写保护,解决了volatile的无原子性,以及synchronized阻塞线程的问题。