Java并发 之 深入理解CAS

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的修改过程,还是会修改成功。

image.png

如何解决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);
    }
    ......
}
复制代码

Guess you like

Origin juejin.im/post/7068534103125164040