深入理解CAS

深入理解CAS

我们知道volatile是不能保证原子性的,在这这篇文章中我们用了一个叫AtomicInteger类来解决这个原子性的问题。那行,这些我们就来仔细看看CAS到底是啥玩意~~~

什么是CAS?

顾名思义,CAScompareAndSet())就是比较并交换

我们下面来个demo

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        /*
        compareAndSet(希望与主内存的值,自己要更改的值)
         */
        System.out.println(atomicInteger.compareAndSet(5, 2091) + "\t" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 3001) + "\t" + atomicInteger.get());
    }
}
/*
true	2091
false	2091
*/

通过上述代码注释compareAndSet()的参数含义和运行结果我们都可以猜出:这个方法的意思是假如我的预期值和主存中的值是一样的(都为5),那么我就将该值改变为2091。

所以为什么第一个是true第二个为什么为false了:因为第一次改了之后,你的主存的值就不再是5了,而是2091了,所以就不匹配了,就不能改变新的值了。

剖析CAS

我们知道假上一节说到volatile的时候我们用了 atomicInteger.getAndIncrement()操作来保证了原子性。

那么,这个AtomicInteger类究竟有啥魔力可以让CAS这么听话地保证了原子性?

那么我们就来看看这个 getAndIncrement() 源码的方法究竟是啥?

getAndIncrement()方法:参数解释(this:当前对象,valueOfferset:就是地址偏移量,1:就是你所要自增的数值)

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

unsafe类中的getAndAddInt()方法:参数解释跟上面无异,只不过这里需要知道这里用到的是自旋的do{} while()操作

public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         // 每次都根据对象和地址偏移量去主存拿数值 赋值给v
         v = getIntVolatile(o, offset);
         // 如果期望值o和当前v值是一样的,就进行更新值v + delta操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
}

compareAndSwapInt()方法:底层用到的就是该CAS的思想,靠的是CPU指令并发原语来保证线程安全性的

public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

所以上述流程解释为:(这算是第二次解释了)假设线程A和B两个线程同时执行getAndAddInt操作:

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicIntegervalue为3,根据JMM内存模型,线程A和线程B各自持拷贝了value为3的副本到自己的工作内存了;
  2. 线程A通过getIntVolatile(var 1,var 2)获取到value为3,然后被挂起;
  3. 线程B也通过getIntVolatile(var 1,var 2)获取到value为3,但是没有被挂起,然后线程B就开始执行compareAndSwapInt()比较发现自己工作内存中的值和主内存的值比较是一样的,就将数值改为4,然后线程B收工;
  4. 线程A恢复,执行compareAndSwapInt()发现自己的工作内存的值和主内存的值4不一致了,说明该值已经被其他线程修改了,所以线程A修改失败,只能重新读取重来一遍了。

那CAS有啥缺点呢?

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

  2. 只能保证一个共享变量的原子操作;( v是不是代表一个?)

    public final int getAndAddInt(Object o, long offset, int delta) {
         int v;
         do {
             v = getIntVolatile(o, offset);
         } while (!compareAndSwapInt(o, offset, v, v + delta));
         return v;
    }
    
  3. 会出现ABA问题;

什么是ABA问题?

直接看图:

img

下面我们直接看一个demo

public class AtomicStampReference {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("===========以下是ABA问题的产生===========");
        AtomicInteger atomicInteger = new AtomicInteger(10);
        new Thread(() -> {
            System.out.println(atomicInteger.compareAndSet(10, 100));
            System.out.println(atomicInteger.compareAndSet(100, 10));
            System.out.println(atomicInteger.compareAndSet(10, 100));
        }, "t1").start();

        new Thread(() -> {
            System.out.println(atomicInteger.compareAndSet(100, 10) + " " + atomicInteger.get());
        }, "t2").start();
        Thread.sleep(2000);
    }
}

上述的代码运行起来是没问题的,证明CAS确实没办法解决这个ABA问题(中间线程已经将主内存操作很多次了,只是最后的时候的那个值还是和第一个值一样,所以要规避这个问题),那么如何解决呢?

如何解决ABA问题?

这里我们需要认识一个新的知识:原子引用 AtomicReference

下面来个AtomicReference类的demo

class User{
    int age;
    String name;

    public User(String name,int age) {
        this.name = name;
        this.age = age;
    }
    get()/set()。。。
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("z3", 23);
        User li4 = new User("li4",24);

        AtomicReference<User> reference = new AtomicReference<>();
        reference.set(z3);
        System.out.println(reference.compareAndSet(z3, li4) + " " + reference.get().toString());
        System.out.println(reference.compareAndSet(z3, li4) + " " + reference.get().toString());
    }
}
/*
true User{age=24, name='li4'}
false User{age=24, name='li4'}
*/

玩法就是跟AtomicInteger类相似。

延申出来,如何解决这个ABA问题呢?就需要用到了时间戳引用,即修改版本号 AtomicStampReference

public class AtomicStampReference {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("===========以下是ABA问题的解决===========");
        new Thread(() -> {
            try {
                // 获取时间戳
                int stamp = stampedReference.getStamp();
                stampedReference.compareAndSet(10, 100, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "\t第一次获取的版本号:" + stamp);
                Thread.sleep(1000);
                stampedReference.compareAndSet(100, 10, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t第二次获取的版本号:" + stampedReference.getStamp());
                stampedReference.compareAndSet(10, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t第三次获取的版本号:" + stampedReference.getStamp());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t3").start();


        new Thread(() -> {
            try {
                // 获取时间戳
                int stamp = stampedReference.getStamp();
                System.out.println(Thread.currentThread().getName() + "\t第一次获取的版本号:" + stamp);
                Thread.sleep(3000);
                boolean result = stampedReference.compareAndSet(100, 10, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "\t第二次获取的版本号:" + stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t4").start();
    }
}
/*
===========以下是ABA问题的解决===========
t3	第一次获取的版本号:1
t4	第一次获取的版本号:2
t3	第二次获取的版本号:3
t3	第三次获取的版本号:4
t4	第二次获取的版本号:41
t4	修改成功否:false
*/

综上,AtomicStampReference就可以解决ABA问题啦~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值