面试篇 —— 谈谈你对CAS的理解

一、CAS是什么

比较后交换,为了保证原子性而进行的比较和交换。

二、CAS的使用

前面说到volatile关键字是不保证原子性的,为了满足轻量级的JMM原则,可以通过volatile + CAS实现轻量级的JMM原则(保证数据可见性、保证原子性、禁止指令重排以保证有序性原则)。例如:

public class VolatileDemo {
    // 定义volatile修饰的原子包装类
    public volatile AtomicInteger number = new AtomicInteger();
    //  实现原子整型包装类的自增i++
    public void atomicDemo() {
        number.getAndIncrement();
    }
}
    /**
     * 测试20个线程各执行1000次自增后结果
     */
    @Test
    public void atomicDemo() {
        VolatileDemo volatileDemo = new VolatileDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    volatileDemo.atomicDemo();
                }
            }, String.valueOf(i)).start();
        }
        // 等自建的所有线程执行完成后再执行以下代码,因为程序默认存在main线程和GC垃圾回收线程, 故>2
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(volatileDemo.number.get());
    }

三、CAS原理

在AtomicInteger原子整型包装类方法中,使用的关键class是Unsafe,这个类中的方法是由native修饰的,这是jdk与计算机系统之间数据操作的约定后门,类比Thread中的start方法的底层。这些类存在于jdk本身自带的rt.jar中。

四、CAS缺点

在AtomicInteger中存在方法compareAndSet

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

参数expect是第一次取出的数值(线程从主内存copy到自己的工作内存中),update是在进行数据操作后的数值。当主内存中的数值与expect相同,则将主内存中的数值更新为update数值,并通知其他线程保证可见性,如果不同则不进行更新。这样只关注取出和结果比对的过程,有一个明显的缺陷,那就是ABA问题,ABA问题就是取出时是A,中间不管被其他线程更新多少次,只要在当前线程结果比对之前再变为A,那么就算比对成功,当前线程就还会将其数值认为一直没有变化,对其进行更新。

五、CAS实例

除了AtomicInteger之外,jdk还提供了不同类型的原子型包装类

// 整型原子包装类,默认为0
AtomicInteger atomicInteger = new AtomicInteger();
// 布尔原子包装类,默认返回false
AtomicBoolean atomicBoolean = new AtomicBoolean();
// 自定义类原子包装
AtomicReference<Object> atomicReference = new AtomicReference<>();

AtomicLong atomicLong = new AtomicLong();
// 带有版本号的自定义原子包装类,初始化需要提供版本号
AtomicStampedReference<Object> atomicStampedReference = new AtomicStampedReference<>(null, 1);

六、ABA问题的解决

使用AtomicStampedReference自定义版本原子包装类可以解决ABA问题。原理是,在存到主内存时定义一个stamped版本号,copy值到线程工作内存时连带着版本号,而主内存中的值每更新一次,版本号也跟着自增。当前线程操作完值后进行CAS(比较并交换),此时需要比较值和版本号是否都一致,这样就避免了中间过程中值更新问题。

上一篇:JVM之TLAB


下一篇:原子类