Java

Java 并发编程—CAS 机制

2019-03-30  本文已影响55人  未见哥哥

CAS 机制

Java 并发编程-CAS 机制

什么 CAS ?

在 Java 中,锁分为两类,一种是悲观锁 Synchronized ,一种是乐观锁 CAS 机制

CAS 机制是 Compare And Swap 的缩写,比较替换的操作。是用于在多线程下提供原子性操作

CAS有三个操作数:内存值V旧的预期值A要修改的值B,当且仅当预期值 A 与内存值 V 相等时,则将内存值修改为 B 并返回true,否则返回 false。

不理解不要紧,下面通过 JDK 提供的实现,来看 CAS 是如何实现原子性操作的。

JDK 提供的原子操作类

在 JDK 包 java.util.concurrent.atomic提供很多原子性操作的类。

AtmoicInteger,AtomicLong,AtmoicBoolean...

AtomicIntegerArray...

AtomicReference...

AtomicIntegerFieldUpdater...

atomic

下面基于 JDK 提供 CAS 机制 AtmoicInteger 来修改上面的程序

public class AtomicPersonCount implements Runnable {
    //①
    private AtomicInteger personCount = new AtomicInteger(0);

    public static void main(String[] args) {

        AtomicPersonCount waiter = new AtomicPersonCount();

        Thread person1 = new Thread(waiter);

        Thread person2 = new Thread(waiter);

        person1.start();
        person2.start();


    }

    @Override
    public void run() {

        for (int i = 0; i < 100; i++) {
            //②
            personCount.incrementAndGet();
        }

        System.out.println(Thread.currentThread().getName() + "-人数:" + personCount.get());

    }
}

在上面的程序中主要修改的点有两处,①将变量 personCount 使用 AtomicInteger 来包装,②处使用 personCount.incrementAndGet() 进行累加操作。这样,两个线程进行累加的就一定是200了。

AtomicInteger 的工作原理

上面改造的代码中personCount++操作替换为incrementAndGet,获取线程工作内存的值 A,然后进行累加计算得到 B,然后将 A 和主存 V 进行比较,只有 A 和 V 的值一样的情况,才会将主存中的值 V 更新为 B,否则就不断的循环重试,直到成功,这个过程就自旋CAS。

基于上面这段代码,我们来分析一下 AtomicInteger 的是如何实现原子性操作的?

AtomicInteger 初始化做了什么事?

//AtomicInteger.java
//①
private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;
static {
    try {
        //②
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
//③
private volatile int value;

在 ① 中得到 Unasfe 类对象,观察Unasfe源码,它内部定义了很多 native 方法,AtomicInteger 内部的 CAS 机制就是通过 Unsafe 类来实现的。

valueOffset 是通过 UnsafeobjectFieldOffset 方法获取的,它的作用是可以获取AtomicInteger成员属性 value 在内存中地址相对于对象内存地址的偏移量。简单理解就是 value 这个成员变量在内存中的地址,拿到这个内存地址偏移值之后,后续就可以直接从内存地址位置进行读取。

在③中定义的 value 属性比较好理解,它就是当前 AtomicInteger 保存的值。

内部CAS是如何进行的?

我们通过调用 increamentAndGet() 方法即可给我们的值进行累加。通过观察源码,内部是使用 unsafe.getAndAddInt(...)实现 CAS 的。

//AtomicInteger.java
/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    //内部会进行累加操作,然后返回累加后的值
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//Unsafe.java
//var1表示当前 AtomicInteger 对象
//var2 表示当前 value 在内存的偏移值
//var4 表示当前需要累加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //得到当前线程工作内存的值
        var5 = this.getIntVolatile(var1, var2);
        //将拿到 var5 的值通过 `compareAndSwapInt(...)` 与主存中的值进行比对,如果相等,将内存值替换为 `var5+var4` 并返回 true,表示替换成功。如果不想等,也就是主存中的值被其他线程修改了,那么返回 false,继续循环,再次从主存中获取最新的值,然后赋值给 var5 ,然后再进一次比对,直到成功为止。这个过程就是上面说到的 `CAS自旋`。
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    //③返回累加前值var5。
    return var5;
}

CAS 存在的缺陷

假如一个值原来是A,变成了B,又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

AtomicStampedReference中的compareAndSet方法声明如下:

public boolean compareAndSet(
           V          expectedReference,         // 预期引用
           V          newReference,              // 更新后的引用
           int         expectedStamp,            // 预期标志
           int         newStamp                  // 更新后的标志
)

举个栗子:老张(线程A)来到客厅,发现有点口渴,然后拿一个水杯(对象one)倒了一杯水,这时突然想去上个 WC ,因此放下手中的水杯(对象one)。这时老王(线程B)来了,他刚好也口渴,看到桌上有一杯水,然后就喝下去了,之后拿同一个水杯(对象 one)再去倒一杯水,又放回同样的位置。当老张(线程A)去完 WC 后(抢到 CPU 执行权地线程),发现桌上那杯水还在,那他也把这杯水喝了。

上面这个栗子就是 ABA问题,老张的水中途是被隔壁老王喝了,但是他去完 WC 之后,发现杯子还是那个杯子(还是对象one),这时就错误地认为数据中途没有被改动。

在 JDK1.5 中提供了 AtomicStampedReference 类,来解决 ABA 问题,下面我们来看看这个类是如何使用的?

public class AtomicStampedReferenceABA {

    private static Object one = new Object();


    public static void main(String[] args) {
        //解决 ABA 问题
        final AtomicStampedReference<Object> ai = new AtomicStampedReference<>(one, 0);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //老王来了,看到一杯水,喝掉它
                boolean result = ai.compareAndSet(one, one, 0, 1);
                System.out.println("老王 CAS 是否成功?" + result + " stamp = " + ai.getStamp());
            }
        });

        t1.start();

        try {
            //老张去 WC
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //老张 WC 回来了,这时发现我的水杯别人喝过,这时我们失败
        boolean result = ai.compareAndSet(one, one, 0, 1);
        System.out.println("老张CAS是否成功?" + result + " stamp = " + ai.getStamp());
        try {
            t1.join(1000);
            System.out.println("stamp = " + ai.getStamp());
            System.out.println("对象比较 " + (one == ai.getReference()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

老王 CAS 是否成功?true stamp = 1
老张CAS是否成功?false stamp = 1
stamp = 1
对象比较 true

这里老张的水被老王喝了,所以他从 WC 回来之后,就不能喝了,也就是 compareAndSet 会失败。这样就解决了 ABA 问题了。

总结

本文总结 CAS 机制的原理,以及通过源码的角度来分析内部的实现,然后分析了 CAS 缺陷,并引入了一个隔壁老王喝老张水的栗子来说明 ABA 问题,并通过 AtomicStampedReference 来解决 ABA 问题。

参考

Java 多线程编程核心技术
Java并发编程之CAS原理分析
AtomicInteger 源码解析
AtomicStampedReference解决ABA问题

记录于 2019年3月30号

上一篇 下一篇

猜你喜欢

热点阅读