一、什么是 CAS

一句话:比较并交换 == Compare and Swap

  • CAS(Compare-And-Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
  • CAS 是一种无锁的非阻塞算法的实现。
  • CAS 包含了3个操作数:
    》 需要读写的内存值 V
    》 进行比较的值 A(预估值)
    》 拟写入的新值 B(更新值)
  • 当且仅当V的值等于A时,CAS算法通过原子方式用新值B来更新V的值,否则不会执行任何操作。

二、CAS 的底层原理--Unsafe的理解

//相当于i++操作实现
public final int getAndIncrement(){
     return unsafe.getAndInt(this, valueoffset, 1);
}

以atomicInteger为例,所使用的方法都是Unsafe类的方法,也就是CAS算法使用的是Unsafe类提供的方法。

什么是Unsafe:

Unsafe类是CAS的核心类,由于java方法无法访问底层系统,需要本地方法(native)来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存的数据,Unsafe存在于sun.misc包中,其内部的方法操作可以像C的指针一样直接操作内存,所以java中CAS操作的执行依赖于 Unsafe类的方法。

注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应的任务。

三、模拟CAS算法Demo

package com.zhouzy.base.juc;


public class CasTest {
  public static void main(String[] args) {
    CompareAndSwap cas = new CompareAndSwap();
    for(int i=0;i<10;i++) {
      new Thread(new Runnable() {
        @Override
		public void run() {
		  int expectValue = cas.get();
		  boolean b = cas.compareAndSet(expectValue, (int)(Math.random() * 101));
		  System.out.println("本次更新是否成功:"+b);
		}
	  }).start();
	}
  }
}

class CompareAndSwap {
  private int value;
	
  //获取内存值
  public synchronized int get() {
	return value;
  }
	
  //比较
  public synchronized int compareAndSwap(int expectValue,int newValue) {
	int oldValue = value;
	if(oldValue == expectValue) {
		this.value = newValue;
	}
	return oldValue;
  }
	
  //设置
  public synchronized boolean compareAndSet(int expectValue,int newValue) {
	return expectValue == compareAndSwap(expectValue, newValue);
  }
}

结果:

本次更新是否成功:false
本次更新是否成功:false
本次更新是否成功:true
本次更新是否成功:false
本次更新是否成功:true
本次更新是否成功:true
本次更新是否成功:false
本次更新是否成功:false
本次更新是否成功:true
本次更新是否成功:true

 

四、CAS算法的缺点

1、循环时间长开销大

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们只能使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3、会出现ABA问题

五、什么是ABA问题

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化,也就是说两个线程都读到数据为5,一个线程暂停2秒后,另一个线程把5修改为6然后又修改回5,当第一个线程来到后发现和期望值相同,则修改想要修改的值。

尽管线程的CAS操作成功,但是不代表这个过程就是没问题的。

六、如何解决ABA问题

使用原子引用 + 新增时间戳(修改版本号)

代码演示ABA问题及解决:

/**
 * ABA问题解决
 * @author wannengqingnian
 */
public class TestAtomicStampedReference {

    /**
     * 创建带时间戳的原子引用
     */
    static AtomicStampedReference<Integer> atomicStampedReference
            = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        //启动一个T1线程模拟ABA问题出现
        new Thread(() -> {
            //获取时间戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次时间戳" + stamp);

            //暂停1秒钟T1线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //模拟ABA
            atomicStampedReference.compareAndSet(100, 101,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第一次修改版本号 : "+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第二次修改版本号 : "+atomicStampedReference.getStamp());

        }, "T1").start();

        //启动T2线程验证是否解决ABA问题
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t线程获得的版本号 :"+stamp);

            //暂停3秒,确保T1完成ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //开始修改
            Boolean flag = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + "\t修改是否成功"+flag + "\t此时的版本号" + atomicStampedReference.getStamp());

        }, "T2").start();
    }

}

结果:

T1	第一次时间戳1
T2	线程获得的版本号 :1
T1	第一次修改版本号 : 2
T1	第二次修改版本号 : 3
T2	修改是否成功false	此时的版本号3