CAS 基本概念
CAS是Compare And Swap的简称,也就是比较并交换。通常指的是一种无锁的原子算法:其作用是更新一个变量的值,首先需要比较它的内存值与某个期望值是否相同,如果相同,就给它赋予一个新值。CAS操作直接在用户态对内存进行读写操作,无需用户态与内核态切换,因此CAS操作没有线程阻塞。CAS可以看做是一种乐观锁,Java原子类的递增操作就是通过CAS自旋实现的。
以下伪代码描述了一个由比较和赋值两个阶段组成的复合操作,CAS可以看做是他们合并后的整体,一个不可分割的原子操作:
if(value == expectedValue){
value = newValue;
}
复制代码
CAS 在Java中的实现
在Java中,CAS操作是由Unsafe 类提供支持的,该类定义了三种针对不同类型变量的CAS操作,如下代码:
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);
复制代码
这三个方法都是native方法,其具体实现是由Java虚拟机提供的,不同Java虚拟机对它们的实现可能略有不同。 它们接收的4个参数分别为:对象实例、被修改的字段的内存偏移量、字段期望值、字段新值,这个三个方法会针对指定对象实例中的相应偏移量的字段执行CAS操作,如下示例代码:
public class CASDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Entity entity = new Entity();
Field field = Unsafe.class.getDeclaredField("theUnsafe");//通过反射获取 Unsafe 类中的 theUnsafe 属性,即Unsafe 实例
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
//获取字段的内存偏移量
long offset = unsafe.objectFieldOffset(Entity.class.getDeclaredField("x"));
// 4个参数分别为:对象实例,需要修改的字段的内存偏移量,字段的期望值,字段新值;
// 如果字段的期望值与当前字段内存值相等,则可以修改成功,返回true
boolean successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
System.out.println(successful + "\t" + entity.x);
}
}
class Entity{
int x;
}
复制代码
CAS 缺陷
CAS虽然高效地解决了原子操作,但是还是会存在一些缺陷,主要表现在三个方面:
- 在Java中很多的并发框架使用了自旋CAS来获取相应的锁,也就是会一直循环直到获取到相应的锁后,然后才开始执行相应操作。自旋时CAS会一直占用着CPU资源。如果CAS自旋长时间不成功,就会给CPU带来非常大的开销。
- 只能保证一个共享变量原子操作 -- 对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,自旋CAS就无法保证操作的原子性,这个时候就需要用锁。
- ABA问题
ABA 问题
CAS算法实现的一个重要前提就是需要提取出内存中某时刻的数据,而在下一时刻比较并替换,因此在这个时间差内,数据可能发生变化。
什么是ABA问题
所谓的ABA问题就是:当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值从A修改为B,又马上将其修改为A,此时其他线程读取到的值还是A,感知不到A->B->A的修改过程,还是会修改成功。
如何解决ABA问题
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。 Java中提供了一个原子类AtomicStampedReference 来解决ABA问题,该类的部分代码如下:
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;
/**
* Creates a new {@code AtomicStampedReference} with the given
* initial values.
*
* @param initialRef the initial reference
* @param initialStamp the initial stamp
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
//其他代码省略.....
}
复制代码
AtomicStampedReference 的构造方法传入了两个参数,V initialRef 即是我们实际存储的初始变量,int initialStamp 初始版本号。 Pair.reference 就是我们实际存储的变量,Pair.stamp 是版本号,每次修改都通过stamp+1来保证版本的唯一性。这样就可以保证每次修改后的版本也会往上递增。
如下代码示例,执行完结果可以得知Thread1无法成功修改,因为版本号不一致。
public class AtomicStampedReferenceDemo {
public static void main(String[] args) {
//初始化AtomicStampedReferenct , 初始值为1, 初始版本号为1
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(1,1);
new Thread(() -> {
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("Thread1 read value:" + value + ", stamp:" + stamp);
//阻塞1秒
LockSupport.parkNanos(1000000000L);
//通过CAS修改value为3
if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp+1)){
System.out.println("Thread1 update value from " + value + " to 3");
}else {
System.out.println("Thread1 update fail.");
}
}, "Thread1").start();
new Thread(() -> {
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("Thread2 read value:" + value + ", stamp:" + stamp);
//通过CAS修改value为2
if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp+1)){
System.out.println("Thread2 update value from " + value + " to 2");
value = (int) atomicStampedReference.get(stampHolder);
stamp = stampHolder[0];
System.out.println("Thread2 read value:" + value + ", stamp:" + stamp);
//通过CAS修改value为1
if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp +1)){
System.out.println("Thread2 update value from " + value + " to 1");
}
}else {
System.out.println("Thread2 update fail.");
}
}, "Thread2").start();
}
}
复制代码
Java 也提供了另外一个类 AtomicMarkableReference,可以理解为是AtomicStampedReference的简化版,因为它不关心修改过多少次,只关心是否修改过。使用boolean类型变量mark 记录值是否有过修改。
public class AtomicMarkableReference<V> {
private static class Pair<T> {
final T reference;
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}
private volatile Pair<V> pair;
/**
* Creates a new {@code AtomicMarkableReference} with the given
* initial values.
*
* @param initialRef the initial reference
* @param initialMark the initial mark
*/
public AtomicMarkableReference(V initialRef, boolean initialMark) {
pair = Pair.of(initialRef, initialMark);
}
......
}
复制代码