Java多线程——CAS与原子操作类

synchronized 属于悲观锁,CAS(Compare and Swap,比较并交换)则属于乐观锁,是一种高效实现线程安全性的方法,支持原子更新操作,适用于计数器等场景。CAS 操作失败时由开发者决定是继续尝试,还是执行别的操作,因此支持失败的线程不会被阻塞挂起。

CAS原理

CAS 机制使用了 3 个基本操作数:内存地址 V,预期原值 A 和新值 B。

更新一个变量的时候,只有当变量的预期原值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B,否则 CPU 不做任何操作。

这样说或许有些抽象,我们来看一个例子:

  1. 在内存地址 V 当中,存储着值为 10 的变量;
  2. 此时线程 1 想要把变量的值增加 1,对线程 1 来说,旧的预期值 A=10,要修改的新值 B=11;
  3. 在线程 1 要提交更新之前,另一个线程 2 抢先一步,把内存地址 V 中的变量值率先更新成了 11;
  4. 线程 1 开始提交更新,首先进行 A 和地址 V 的实际值比较(Compare),发现 A 不等于 V 的实际值,提交失败;
  5. 线程 1 重新获取内存地址 V 的当前值,并重新计算想要修改的新值,此时对线程 1 来说,A=11,B=12,这个重新尝试的过程被称为自旋;
  6. 这一次没有其他线程改变地址 V 的值,线程 1 进行 Compare,发现 A 和地址 V 的实际值是相等的;
  7. 线程 1 进行 SWAP,把地址 V 的值替换为 B,也就是 12。

那么在 Java 中都有哪些地方用到了 CAS 机制呢?

有许多地方用到,包括上面说的 Atomic 操作类以及 Lock 系列类的底层实现。甚至在 JDK1.6 以上版本,synchronized 转变为重量级锁之前,也会采用 CAS 机制。

AtomicInteger源码分析

AtomicInteger 是通过 sun.misc.Unsafe 这个硬件级别的原子操作类来实现 CAS 操作的。下面看下 AtomicInteger.java 的部分实现:

package java.util.concurrent.atomic;
import sun.misc.Unsafe;

public class AtomicInteger extends Number implements java.io.Serializable {
    // 取得Unsafe对象
    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; // 初始Integer大小
    // 省略了部分代码...

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    // 无参数构造函数, 默认初始大小为0
    public AtomicInteger() {
    }
    // 返回旧值, 并设置新值为newValue
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    // 获取当前值, 并设置新值+1, 同value++
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    // 省略了部分代码...
}

看下 getAndSet() 中 unsafe.getAndSetInt() 方法的实现:

public final class Unsafe {
    public final int getAndSetInt(Object o, long offset, int delta) {
        int v;
        do {
            // 获得当前主内存中的值
            v = this.getIntVolatile(o, offset); 
        // CAS 操作更新值, 如果失败就一直重试
        } while(!this.compareAndSwapInt(o, offset, v, delta));
        return v;
    }
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    // 省略了部分代码...
}
CAS总结

CAS 多数情况下对开发者来说是透明的:

  • JUC 的 atomic 包(java.util.concurrent.atomic)提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选;
  • Unsafe 类虽然提供了 CAS 操作,但因能够操作任意内存地址读写而有隐患;
  • Java 9 及以上,可以使用 Variable Handle API 来替代 Unsafe 进行各种粒度的原子或者有序性的操作。

CAS 缺点:

  • 若更新一个变量,一直更新不成功而进行循环,则 CPU 开销很大;
  • 能保证代码块的原子性,只能保证一个共享变量的原子操作;
  • ABA 问题,这是 CAS 机制最大的问题所在。

什么是 ABA 问题?怎么解决?

如果地址 V 初次读取的值是 a,并且在准备赋值的时候检查到它的值仍然为 a,我们就可以说它的值没有被其他线程改变过了吗?是不能的,有可能中间被改成了 b,又改回了 a。而 CAS 操作就会认为从来没有被改过。这个问题就是 CAS 的 ABA 问题。

JUC 为了解决 ABA 问题,提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过变量值的版本来保证 CAS 的正确性。

在使用 CAS 前要考虑好 ABA 问题是否影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的 synchronized 可能会比原子类更高效。

猜你喜欢

转载自blog.csdn.net/lwl2014100338/article/details/107871153