CAS原子类:AtomicInteger源码解析

Published on 2024-09-27 17:21 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、AtomicInteger的使用

先回顾下AtomicInteger常用方法:

方法名 方法描述
public final int get() 获取当前对象的值
public final int getAndSet(int newValue) 设置新值并返回旧值
public final boolean compareAndSet(int expect, int update) CAS方式设置新值
public final int getAndIncrement() 自增并返回旧值
public final int getAndDecrement() 自减并返回旧值
public final int getAndAdd(int delta) 加法并返回旧值
public final int addAndGet(int delta) 加法并返回新值

下面是使用AtomicInteger实现多线程自增的案例:

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author kdyzm
 * @date 2024/9/25
 */
@Slf4j
@Data
public class AtomicIntegerTest implements Runnable {

    private AtomicInteger count = new AtomicInteger(0);
    private CountDownLatch countDownLatch = new CountDownLatch(10);

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerTest atomicIntegerTest = new AtomicIntegerTest();
        for (int i = 0; i < 10; i++) {
            CompletableFuture.runAsync(atomicIntegerTest);
        }
        atomicIntegerTest.getCountDownLatch().await();
        log.info("十个线程共自增十万次结果:{}", atomicIntegerTest.getCount().get());
    }

    @Override
    public void run() {
        log.info("正在执行任务:{}", Thread.currentThread().getName());
        for (int j = 0; j < 10000; j++) {
            count.incrementAndGet();
        }
        countDownLatch.countDown();
    }
}

二、源码解析

AtomicInteger原子类的原理是什么,为什么能保证多线程自增不会出现错误呢?查看源代码一探究竟

public class AtomicInteger extends Number {
    //获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //value字段偏移量
    private static final long valueOffset;

    //初始化value字段偏移量
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    //volatile关键字修饰保证可见性
    private volatile int value;

    //构造函数
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    //设置新值,返回旧值
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    //CAS方式设置新值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    //自增并返回新值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    //自增并返回旧值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
	
    //相加并返回旧值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    
    //相加并返回新值
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    //其它方法。。。。。。
}

可以看到,AtomicInteger的内部实现和我们之前手动实现的使用Unsafe类实现的多线程自增的案例很像了(详细参考:《线程同步机制二:CAS原理和JUC原子类》),内部都是使用Unsafe+volatile关键字搭配的方式实现的相关功能,只是这里调用的Unsafe类的方法更具有定制性,不再是通用的compareAndSwap,而是compareAndSwapInt或其他方法,应该是考虑到有更高的效率要求。

再详细说下该类中的源码

1、静态代码块

//初始化value字段偏移量
static {
    try {
        valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
        throw new Error(ex);
    }
}

静态代码块在类加载的时候初始化了valueOffSet的值,valueOffSet是字段偏移量,实际上就是从对象头到该字段的字节数(详细参考:《深入理解Java对象结构》),使用JOL工具打印出AtomicInteger的对象结构看看,运行以下代码:

@Test
public void testAtomicIntegerStructure() {
    AtomicInteger atomicInteger = new AtomicInteger(1);
    //打印虚拟机信息
    System.out.println(VM.current().details());
    //打印内存结构
    System.out.println(ClassLayout.parseInstance(atomicInteger).toPrintable());
}

输出结果:

image-20240927151009770

在类加载的时候就计算出来value字段的偏移量保存到valueOffset字段,以便后续使用Unsafe对value字段快速寻址。

2、value字段

//volatile关键字修饰保证可见性
private volatile int value;

value字段使用了volatile关键字进行进行了修饰,因为它作为共享变量会被多线程读写,如果没有可见性保证,可能会出现数据不一致的情况。

3、调用Unsafe类的几个方法

我将调用Unsafe类的几个方法划分成了以下几类:非CAS方法、无自旋CAS方法、自旋CAS方法,这里的“自旋”就是循环重试的意思。

3.1 非CAS方法

典型的代表是objectFieldOffset方法

public native long objectFieldOffset(Field field)

静态代码块中的类就是调用了该方法用于计算目标类的字段在目标对象中的内存偏移量。返回值即为相对于对象头的偏移字节数。

3.2 无自旋CAS方法

这一类在AtomicInteger类中只调用了compareAndSwapInt方法

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

usafe的compareAndSwapInt方法定义如下

public final native boolean compareAndSwapInt(Object obj, long offset, int exp, int update);
  • obj:操作的目标对象,在这里其实就是AtomicInteger本身
  • offset:之前调用objectFieldOffset计算出来的value字段偏移字节
  • exp:期望值,cas会拿它和内存中的值做比较
  • update:若是exp期望值和内存中的值相同,表示无人更新该字段,就会用update值更新到内存。

需要注意的是,compareAndSwapInt方法只会CAS操作一次,若是成功就返回true,若是失败就返回false。

3.3 自旋CAS方法

这一类在AtomicInteger类中方法比较多,以下方法均是

public final int getAndSet(int newValue) ;
public final int getAndIncrement();
public final int getAndDecrement();
public final int getAndAdd(int delta) ;
public final int incrementAndGet();
public final int decrementAndGet() ;
......

这些方法和compareAndSwapInt方法最大的不同之处就是:Unsafe内部都进行了自旋以确保CAS操作成功,并且还返回了操作前或者操作后的值。这些方法底层都调用了Unsafe类的public final int getAndAddInt(Object var1, long var2, int var4)方法,源码如下:

public final int getAndAddInt(Object obj, long offset, int update) {
    int expect;
    do {
        expect = this.getIntVolatile(obj, offset);
    //CAS自旋直到成功
    } while(!this.compareAndSwapInt(obj, offset, expect, expect + update));
    return expect;
}

这就和之前手动实现的cas逻辑(https://blog.kdyzm.cn/post/261#2%E4%BD%BF%E7%94%A8unsafe%E7%B1%BB%E5%AE%9E%E7%8E%B0%E5%A4%9A%E7%BA%BF%E7%A8%8B%E8%87%AA%E5%A2%9E)几乎一样了。

4、java8新增的方法

java8有个很亮眼的功能就是函数式编程,AtomicInteger类也很合时宜的引入了相关的方法:

public final int getAndUpdate(IntUnaryOperator updateFunction);
public final int updateAndGet(IntUnaryOperator updateFunction);
public final int getAndAccumulate(int x,IntBinaryOperator accumulatorFunction);
public final int accumulateAndGet(int x,IntBinaryOperator accumulatorFunction);

以getAndAccumulate为例

public final int getAndAccumulate(int x,
                                  IntBinaryOperator accumulatorFunction) {
    int prev, next;
    do {
        prev = get();
        next = accumulatorFunction.applyAsInt(prev, x);
    } while (!compareAndSet(prev, next));
    return prev;
}

就是把新值改成了通过IntBinaryOperator函数计算了。。其实没什么东西,非常简单直观的逻辑。

三、源码中有趣的几个点

第一个点:

看下源码中的incrementAndGet以及getAndIncrement方法

//自增并返回新值
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//自增并返回旧值
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

两个方法的实现几乎一模一样,只有返回值返回新值还是旧值的区别,返回新值的时候直接+1而非重新获取值,一则是因为原子类是一种乐观策略的方式实现的,它坚信自己一定能成功;二则如果重新获取值,则由于打破了原子性操作的原则,有可能重新获取的值并非本次操作的最终值。

相似的方法还有getAndAdd以及addAndGet方法等。

第二个点:

compareAndSet方法以及weakCompareAndSet方法

image-20240926101651316

据我观察,这两个方法不是一模一样的实现吗。。为什么会分成两个方法😳



END.


#java #多线程编程
目录