Java 并发编程—CAS 机制

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.incrementAndGet
//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.getAndAddInt
//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 存在的缺陷

  • ABA问题

假如一个值原来是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 问题了。

  • 在 incrementAndGet() 中可以看出,如果自旋时间过程长,就会给 CPU 带来非常大的执行开销。

  • Atomic 原子性操作只能保证一个共享变量的原子性。

总结

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

参考

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

记录于 2019年3月30号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值