CAS(Compare-and-Swap)详解

CAS概述

CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题。

volatile不保证原子性问题
根据我前面volatile文章,volatile并不能保证原子性操作,在并发情况下对i++操作,volatile也会发生丢数据的情况,主要是因为线程在对各自的工作非常对数据进行修改后,准备写到主内存的时候,别的线程已经抢先一步对主内存的数据进行修改,导致数据的丢失。那么如何解决这种问题呢?

使用原子整型类解决volatile对i++操作丢数据问题
代码如下

public class AtomicTest02 {
    // 相当于i = 0,
    AtomicInteger atomicInteger = new AtomicInteger(0);
    public void addPlus() {
        // 相当于i++
        atomicInteger.getAndIncrement();
    }

    public static void main(String[] args) {
        AtomicTest02 atomicTest02 = new AtomicTest02();
        // 启动20个线程对atomicInteger进行++操作
        for (int i = 0; i< 20; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        atomicTest02.addPlus();
                    }
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(atomicTest02.atomicInteger.get());
    }
}

运行结果:

20000

没有出现丢数据了,所以在多线程下,可以使用atomicd等原子类来防止丢数据

CAS工作原理

假如线程在自己的工作空间对数据进行修改后,准备写入主内存时,线程先判断(比较)主内存现有的值是不是自己修改前的那个值,如果是的话那么就直接将数据写入(交换)主内存,不是的话那么就重新读入 主内存的值在进行操作,直到准备写入的时候主内存的值是自己预期的值,这就是比较并交换的简单思想.

CAS代码示例一

public class AtomicTest01 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        // 第一次修改,期望值是0,就修改为1
        boolean b = atomicInteger.compareAndSet(0, 1);
        System.out.println("修改状态:" + b + "结果:" + atomicInteger.get());
        
        //第二次修改,期望值也是0,就修改为2
        boolean c = atomicInteger.compareAndSet(0,2);
        System.out.println("修改状态:" + c + "结果:" + atomicInteger.get());
    }
}

运行结果:

第一次修改状态:true结果:1
第二次修改状态:false结果:1

从结果可以看出 第一次修改成功,把值从0修改为1,但是第二次修改预期值是0,但是实际的值已经从0修改为了1,所以第二次修改就没有修改成功

CAS底层原理
  1. CAS的全程是Compare-And-Swap,它是一条CPU并发原语
  2. 它的功能是判断内存某个位置是否是预期值,如果是则更改为新的值,这个过程是原子性的
  3. CAS并发原语在 java的体现就是sun.mic.Unsafe类个各个方法,调用Unsafe类的方法,JVM会帮助我们实现CAS汇编指令。这是一个完全依赖于硬件的功能,通过它实现原子性操作。由于CAS是一种系统原语,由若干指令组成,该原语执行必须连续的不许中断。

Unsafel类

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

我们来看下源码:
AtomicInteger类的getAndIncrement()方法,类似i++

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

	

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;
    }
  1. 从源码可以看出,getAndIncrement()方法调用了Unsafe类的getAndAddInt(Object var1, long var2, int var4)方法,

    1. 第一个参数:var1是指AtomicInteger对象
    2. 第二个参数:var2表示AtomicInteger对象的内存地址偏移量,即该对象在内存地址的位置
    3. 第三个参数:var4递增的值,在该方法是1
  2. Unsafe类的getAndAddInt方法体是一个do…while循环,该方法主要是判断当前的对象的预期值(即var5)在主内存中是否一样,通过compareAndSwapInt(var1, var2, var5, var5 + var4)来判断对象的预期值是否一样,如果不一样则一直执行do…while循环,直到预期值和主内存的值一致才结束

CAS的缺点

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。

  1. 循环时间长开销很大。
  2. 只能保证一个共享变量的原子操作。
  3. ABA问题。
  • 循环时间长开销很大
    我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

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

  • ABA问题

  1. 如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

  2. 如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。

  • ABA问题的解决
    Java并发包为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

AtomicReference和AtomicStampedReference

AtomicReference:是原子引用类,如果我们自定义的类要解决原子操作可以使用此类(使用泛型),
AtomicStampedReference:是带有版本号的原子引用类,使用此类可以解决CAS的ABA问题

代码示例:

// 原子引用类测试,解决ABA问题
public class AtomicStampedReferenceTest {


    public static void main(String[] args) throws InterruptedException {
        // 注意构造方法的2个参数,第一个参数是引用值,第二个参数是版本号
        AtomicStampedReference<Integer> data = new AtomicStampedReference<>(0,1);

        // 该线程执行一次ABA操作,即把data从0,改为1,在从1改为0
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 版本号
                int stamp = data.getStamp();
                System.out.println("线程t1修改前的版本号是:" + stamp);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程t1先把值改为1,再把值改为0");
                // 把值从0改为1
                // 参数1:期望的引用值,参数2:新的引用值,参数3:期望的版本号,参数3:新的版本号
                data.compareAndSet(0,1, stamp,stamp + 1);
                // 在把值从1改为0,
                int stamp1 = data.getStamp();
                data.compareAndSet(1, 0, stamp1,stamp1 + 1);
            }
        }, "t1").start();

        // 主线程
        int stamp = data.getStamp();
        System.out.println("主线程修改前的版本号是:" + stamp);
        Thread.sleep(2000);
        boolean b = data.compareAndSet(0, 2019, stamp, stamp + 1);
        System.out.println("主线程是否修改成功:" + b + " 当前的版本号是:"+data.getStamp()+" 当前的值是:"+ data.getReference());
    }
}

运行结果:

主线程修改前的版本号是:1
线程t1修改前的版本号是:1
线程t1先把值改为1,再把值改为0
主线程是否修改成功:false 当前的版本号是:3 当前的值是:0

从结果可以看出,主线并没有修改成功,虽然data的值依然 是0,但是因为data的版本号已经不是预期值了,已经被线程t1进行了一次ABA操作,

所以可以利用AtomicStampedReference类的带有版本号的特征来解决CAS的ABA问题。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值