JUC原子类:CAS详解

1,500 阅读8分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

往期推荐

一、CAS

1. 什么是CAS?

CASCompare And Swap的缩写,翻译成中文就是比较并交换,其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新。

CAS操作是原子性的操作(读和写两者同时具有原子性), 所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

其实现方式是通过借助C/C++调用CPU指令完成的,所以效率很高。

简单来说CAS操作涉及三个值,分别是待修改的值E,要修改的新值V,以及待修改值所对应的内存位置的取值N。

1233.png

(1)首先读取并记录待修改的值E
(2)然后计算新值V
(3)比较E和该内存位置的值,如果相等->(4); 如果不相等->(5);
(4)更新为新值
(5)更新失败

2. CAS的使用

如果不使用CAS,在高并发下,多线程同时修改一个变量的值我们需要synchronized加锁

public class Test {
    private int i=0;
    public synchronized int add(){
        return i++;
    }
}

java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。

public class Test {
    private  AtomicInteger i = new AtomicInteger(0);
    public int add(){
        return i.addAndGet(1);
    }
}

3. CAS问题

CAS虽然高效的实现了原子性操作,但是也存在一些缺点,主要表现在以下三个方面。

  • 循环时间长开销大

如果CAS操作失败,就需要循环进行CAS操作(循环同时将期望值更新为最新的),如果长时间都不成功的话,那么会造成CPU极大的开销。

解决方法: 限制自旋次数,防止进入死循环。

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

解决方法: 如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,或者可以把多个共享变量合并成一个共享变量进行CAS操作。

  • ABA问题

在多线程场景下CAS(比较并交换)会出现ABA问题,比如当有两个线程同时去修改变量的值,线程1和线程2都将变量由A改为B。首先线程1获得CPU的时间片,线程2由于某些原因被挂起,线程1经过CAS(Comparent and Swap)将变量的值从A改为B,线程1更新完变量的值后,此时恰好有线程3进来了,线程3通过CAS(comparent And Swap)将变量的值由B改为A,线程3更新完成后,线程2获取时间片继续执行,通过CAS(comparent And Swap)将变量的值由A改为B,而此时的线程2并不知道该变量已经有了A->B->A改变的过程。这就是CAS中的ABA问题。

解决方法:使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。

二、UnSafe类详解

2.1 UnSafe类简介

Unsafesun.misc 下,顾名思义,这是一个不安全的类,因为Unsafe类所操作的并不属于Java标准,Java的一系列内存操作都是交给jvm的,而Unsafe类却能有像C语言的指针一样直接操作内存的能力,同时也会带来了指针的问题。过度使用Unsafe类的话,会使出错率变得更大,因此官方才命名为Unsafe,并且不建议使用,连注释的没有。

UnSafe类总体功能图:

31.png

如上图所示,Unsafe类的功能主要分为内存操作、CAS、Class相关、对象操作、数组相关、内存屏障、系统相关、线程调度等功能。

2.2 UnSafe与CAS

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
  }

  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
  }

  public final int getAndSetInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
    return i;
  }

  public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
    return l;
  }

  public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2)
  {
    Object localObject;
    do
      localObject = getObjectVolatile(paramObject1, paramLong);
    while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
    return localObject;
  }

源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。

又从Unsafe类中发现,原子操作其实只支持下面三个方法。

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

Unsafe只提供了3种CAS方法:

  1. compareAndSwapObject
  2. compareAndSwapInt
  3. compareAndSwapLong

典型应用

CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

5.png

下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地址valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。

6.png

2.3 UnSafe的其他功能

public native long staticFieldOffset(Field paramField);
/*用来获取给定的 paramField 的内存地址偏移量*/
public native int arrayBaseOffset(Class paramClass);
/*用来获取数组第一个元素的偏移地址*/
public native int arrayIndexScale(Class paramClass);
/*用来获取数组的转换因子即数组中元素的增量地址*/
public native long allocateMemory(long paramLong);
/*用来分配内存*/
public native long reallocateMemory(long paramLong1, long paramLong2);
/*用来扩充内存*/
public native void freeMemory(long paramLong);
/*用来释放内存*/

三、CAS原子类AtomicInteger

AtomicInteger是java.util.concurrent.atomic 包下的一个原子类,该包下还有AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子类,主要用于在高并发环境下,保证线程安全。

3.1 常用方法(API)

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

3.2 案例代码

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo implements Runnable {
    private static final AtomicInteger atomicInteger = new AtomicInteger();

    //增加指定数量
    public void getAndAdd() {
        atomicInteger.getAndAdd(-90);
    }
    //增加1
    public void getAndIncrement() {
        atomicInteger.getAndIncrement();
    }
    //减少1
    public void getAndDecrement() {
        atomicInteger.getAndDecrement();
    }
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo r = new AtomicIntegerDemo();
        Thread t1 = new Thread(r);
        t1.start();
        t1.join();
        System.out.println("AtomicInteger操作结果:" + atomicInteger.get());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            getAndDecrement();
        }
    }
}

3.3 源码解析

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            //用于获取value字段相对当前对象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //返回当前值
    public final int get() {
        return value;
    }

    //递增加detla
    public final int getAndAdd(int delta) {
        //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //递增加1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

我们可以看到 AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。

  • volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
  • CAS 保证数据更新的原子性。

四、结言

由于作者水平有限, 欢迎大家能够反馈指正文章中错误不正确的地方, 感谢 🙏

小伙伴的喜欢就是对我最大的支持, 如果读了文章有所收获, 希望能够 点赞、评论、关注三连!

参考文献