> 文档中心 > 并发类编程—CAS机制

并发类编程—CAS机制

目录

  • 1、概念
  • 2、原理
  • 3、缺点
  • 4、ABA问题
  • 5、解决ABA问题

1、概念

CAS(Compare And Swap): 比较并替换,它是一条CPU原语,是一条原子指令(原子性)。
CAS通过比较真实值与预期值是否相同,如果是则进行修改,Atomic原子类底层就是使用了CAS,CAS属于乐观锁。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么会自动将该内存位置的值更新为新值,反之不做任何操作。

2、原理

CAS执行依赖于Unsafe类,Unsafe类的所有方法通过native修饰,所以Unsafe类直接操作系统底层的数据地址,而不通过JVM实现。
比如:A、B线程通过AtomicInteger同时对变量进行自增

    public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta);    }// 使用了unsafe类    public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do {     // 获取原值     var5 = this.getIntVolatile(var1, var2);     // 将原值与预期值进行对比,一致则进行相加var4 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;    }

伪代码流程解释:
A线程将变量num从1进行自增,当执行时没有其他线程修改过num值,所以一次成功,num自增后将值返回给主内存。
B线程也将num进行自增,但是num已经被A线程进行了修改,所有将再次执行do-while,重新获取num的值。
由于JMM模型的可见性,B线程重新获取值时会从主内存中获取到被A修改后的最新值。
通过最新值与期望值进行比较,满足条件就返回true。

CAS如何实现:
通过Unsafe类对系统底层的数据地址进行原子性操作,对比内存地址的值和预期值是否一样,如果一样进行修改。

3、缺点

  • 1、循环时间长,增加了CPU的开销。
  • 2、只能保证一个变量的原子操作。
  • 3、会导致ABA问题。

4、ABA问题

如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的,因为内存X中的值从A到B再到了A。

CAS执行时,将过去某时刻的值与当下时刻进行比较并替换,在这时间差中,值可能会发生多次修改,只是最终值的结果不变。

5、解决ABA问题

为了防止在值比较时,存在被修改过的可能,通过为值加上版本号的方式,在最后执行CAS时判断版本号,确保不会出现ABA问题。

通过原子引用(AtomicReference)加时间戳原子引用(AtomicStampedReference)解决ABA问题。

代码如下:

public class Test {    private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);    public static void main(String[] args) { System.out.println("=========以下ABA问题产生:========="); // 线程 t1 模拟ABA问题的产生 new Thread(() -> {     // 进行一次修改,将值修改为101     atomicReference.compareAndSet(100, 101);     // 进行第二次修改,将值修改回100     atomicReference.compareAndSet(101, 100); }, "t1").start(); new Thread(() -> {     // 线程t2暂停1S,保证线程t1执行完成     try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}     System.out.println(atomicReference.compareAndSet(100, 102));     System.out.println("修改成功,修改后值为:"+atomicReference.get()); }, "t2").start(); try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();} System.out.println("=========以下ABA问题解决方式:========="); new Thread(() -> {     // 获取版本号     int stamp = atomicStampedReference.getStamp();     System.out.println("线程名称:"+Thread.currentThread().getName()+",第一次版本号:"+stamp);     try {TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}     // 进行一次修改,将值修改为101     atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);     stamp = atomicStampedReference.getStamp();     System.out.println("线程名称:"+Thread.currentThread().getName()+",第二次版本号:"+stamp);     // 进行第二次修改,将值修改回100     atomicStampedReference.compareAndSet(101,100,stamp,stamp+1);     stamp = atomicStampedReference.getStamp();     System.out.println("线程名称:"+Thread.currentThread().getName()+",第三次版本号:"+stamp); }, "t3").start(); new Thread(() -> {     // 获取版本号     int stamp = atomicStampedReference.getStamp();     System.out.println("线程名称:"+Thread.currentThread().getName()+",第一次版本号:"+stamp);     // 线程t4暂停1S,保证线程t3执行完成     try {TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}     boolean andSet = atomicStampedReference.compareAndSet(100, 101, stamp,stamp+1);     System.out.println("线程名称:"+Thread.currentThread().getName()+",修改结果:"+andSet+",第二次版本号:"+stamp+1+"实际版本号:"+atomicStampedReference.getStamp());     System.out.println("当前最新值:"+atomicStampedReference.getReference()); }, "t4").start();    }}

分析:
线程t1中,将值从100修改为了101又修改回了100,但是线程t2却成功更新了值,所以产生了ABA问题。
线程t3中,将值从100修改为了101又修改回了100,并且更新的版本号为3;在线程t4中,将100修改为101时,由于线程t4更新时的版本号为2,但是实际的版本号为3,所以无法更新,解决了ABA问题。

运行结果:
并发类编程—CAS机制