Java多线程并发(7)深入CAS-全是干货!

336 阅读5分钟

CAS

CAS是Java中的一种并发控制技术,全称为Compare And Set\Swap(比较与交换)。

其主要思想是在没有锁的情况下,使用CPU的原子性指令(cmpxchg指令),通过硬件保证了比较-更新的原子性,比较共享变量的值与预期的值是否相等,如果相等则更新此变量的值,否则不做任何操作。而在此过程中,涉及到了三个值,位置内存值(主内存的值),旧的预期值(工作内存从主内存加载的值),更新值,如果位置内存值和旧的预期值不相同,那么本次更新值作废。

CAS操作可以保证并发安全,避免了使用锁带来的性能损失,常被用于性能要求高的并发编程场景中。Java提供了Atomic类中的compareAndSet()方法来实现CAS操作。

原子类

java.util.concurrent.atomic包下的类,都是原子类。

所谓原子类是一组基于CAS操作实现的线程安全的工具类,它提供了一套原子性的操作,用来实现线程安全的并发编程。Java提供的原子类包括AtomicBoolean、AtomicInteger、AtomicLong等,它们支持原子性地进行基本数据类型的读取、赋值、加减、比较等操作。

使用原子类的好处就是,它不同于 synchronized重量级锁,它是轻量级的操作,比之原子性更好,更有效。

原子类的实现原理是基于硬件和JVM底层的CAS指令:

  1. 首先,Atomic类会使用CAS操作读取并更新共享变量的值,当共享变量的值与预期值相同时,CAS指令会将新的值更新到共享变量中并返回true,否则CAS指令返回false;
  2. 其次,在多线程并发访问共享变量时,CAS指令可以保证只有一个线程可以成功更新共享变量,如果多个线程同时执行CAS指令,则只有一个线程会成功更新共享变量的值。
AtomicInteger a = new AtomicInteger();

public void getAtomicInteger(){
    int b = a.get();
    a.set(b++);
}
//当线程调用这个方法的时候,这个变量就能保证原子性,
//并且类的内部属性是private volatile int value;同时兼具可见性和禁重排

其中关于CAS的方法:compareAndSet(int expect,int update)

参数一为期望值,二为更新值。

在底层源码中调用的是Unsafe对象的方法compareAndSwapInt(this,valueOffset,expect,update) 是一个本地方法。

也就是说,它实现CAS的原理其实是靠Unsafe类。

实现原理

我们需要通过Unsafe这个类来了解CAS的实现原理。

首先我们对AtomicInteger作为例子进行介绍。

AtomicInteger

AtomicInteger类中有三个属性,分别是:

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

private volatile int value;

其中value用于记录变量的值,valueOffset记录内存偏移地址(表示对象某个字段在该类内存的偏移量,在这个类中是value属性的偏移地址),unsafe用于调用本地方法。

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

因为这里获取的字段是value,所以记录的是它的偏移地址。

然后通过unsafe调用本地方法从硬件上直接对内存进行计算。

在底层使用CAS的方法主要有两种:

  • getAndSetXxx(xxxx)
  • getAndAddXxx(xxxx)

现在我们就getAndAddInt举例说明

getAndAddInt

Unsafe.getAndAddInt中,代码逻辑如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

AtomicInteger调用它的时候会传入三个参数:unsafe.getAndAddInt(this, valueOffset, 1);

回到源码中,它使用var5记录this+valueOffset的值,然后在while判断条件中执行比较:

public final native boolean compareAndSwapInt
(Object var1, long var2, int var4, int var5);

该方法具有以下参数:

  • var1:需要进行操作的对象。
  • var2:需要进行操作的对象中某个字段的偏移量。
  • var4:期望的值。
  • var5:新的值。

该方法的返回值为一个布尔型,表示操作是否成功。如果原始值与期望值相等(这里再次利用var1+var2的地址取值和var5作比较),则将字段的值更新为新的值(var5+var4),并返回 true退出循环,否则不更新(写入var5,称之为自旋)并返回 false继续循环。

这个方法在Unsafe.cpp中调用了一个Atomic::cmpxchg(xxx,xxx,xxx)的函数。

这里调用的是CPU指令,它的参数都是指令的操作码,于是没有方法体,并且会根据不同的操作系统调用重载的函数。

从这里也可以看出,CAS是通过底层指令实现的,所以容易内存管理出错,在JDK9之后就不建议使用了,如果你是面向硬件的工程师,可以深入学习。

并且可以知道,因为它是一个CPU指令,那么它必定有原子性,是不可中断的操作。

ps:AtomicReference它是一个泛型类,用于扩展AtomicInteger等基本原子类

pps:我们可以利用API方法手动写自旋锁。

CAS缺点

循环时间长、开销大

在轮询期望值的时候,如果一直失败,就一直轮询,这样造成CPU消费。

ABA问题

如果线程1在内存位置取出A,线程2同时在相同位置取出A,并改为B又改为A,然后线程1轮询发现和期望值A一样是A,那么线程1退出轮询。

尽管线程1成功,但是不代表是正确的。

于是诞生了一个解决方案:版本号。在Java中使用了一个API实现:AtomicStampedReference

public class test{
    public static void main(String[] Args){
        Integer number = 1;
        AtomicStampedReference<Integer> stamped = new AtomicStampedReference<>(number,1);
        System.out.println(stamped.getReference()+stamped.getStamp());
        Integer number2 = 2;
        stamped.compareAndSet(number,number2,stamped.getStamp(),stamped.getStamp()+1);
        //这里传入期望值和更新值,期望版本号和更新版本。
        System.out.println(stamped.getReference()+stamped.getStamp());
    }
}

总结

在本节,我们学习了CAS及其基本原理,它使用于多线程情况下,对共享变量的读写保护,解决了volatile的无原子性,以及synchronized阻塞线程的问题。