【Java 并发】【八】【Atomic】【二】AtomicInteger、AtomicBoolean原理

发布时间 2023-04-03 23:20:36作者: 酷酷-

1  前言

这节我们从AtomicInteger这个比较简单的原子类开始,来看看AtomicInteger的底层原理。

2  实测样例对比线程安全性

在说AtomicInteger的底层原理之前呢,我们先来看个例子感受下原子类:

static修饰的共享变量,我们开启两个线程对共享变量进行10000次+1的操作

2.1  Integer的测试样例

// 继承线程类
public class AtomicTest extends Thread {

    // 类变量看作共享变量
    private static int num = 0;

    // 线程工作内容  执行10000次+1
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num++;
        }
    }

    // main方法
    public static void main(String[] args) throws InterruptedException {
        // 两个线程分别去对共享变量执行10000次+1
        AtomicTest demo1 = new AtomicTest();
        AtomicTest demo2 = new AtomicTest();
        demo1.start();
        demo2.start();
        // 等待两个线程都执行完
        demo1.join();
        demo2.join();
        // 打印共享变量
        System.out.println("num = " + AtomicTest.num);
    }
}

2.2  AtomicInteger的测试样例

// 继承线程类
public class AtomicTest extends Thread {

    // 类变量看作共享变量
    private static AtomicInteger num = new AtomicInteger(0);

    // 线程工作内容  执行10000次+1
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            num.addAndGet(1);
        }
    }

    // main方法
    public static void main(String[] args) throws InterruptedException {
        // 两个线程分别去对共享变量执行10000次+1
        AtomicTest demo1 = new AtomicTest();
        AtomicTest demo2 = new AtomicTest();
        demo1.start();
        demo2.start();
        // 等待两个线程都执行完
        demo1.join();
        demo2.join();
        // 打印共享变量
        System.out.println("num = " + AtomicTest.num);
    }
}

3  AtomicInteger原理

我们先通过源码来看一下AtomicInteger内部有哪些属性以及作用是什么:

public class AtomicInteger extends Number implements java.io.Serializable {
    // unsafe对象,可以直接根据内存地址操作数据,可以突破java语法的限制
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 存储实际的值
    private volatile int value;
    // 存储value属性在AtomicInteger类实例内部的偏移地址
    private static final long valueOffset;
    static {
        try {
            // 在类初始化的时候就获取到了value变量在对象内部的偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

(1)首先内部持有一个unsafe对象的实例,Atomic原子类底层的操作都是基于unsafe对象来进行的

(2)然后有一个volatile int value变量,这个value就是原子类实际数值,使用volatile来修饰volatile可以保证并发中的可见性有序性(这里之前讲过volatile可以保证可见性和有序性,不记得的要回去重新看一下哦)

(3)还有一个valueOffset,看看这段代码,其实就是获得value属性在AtomicInteger对象内部的偏移地址的:

valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));

这个value属性相对于AtomicInter对象的内部偏移量存储在valueOffset中,我们之前讲过的,通过unsafe类是直接在内存级别去给变量赋值的。这里啊,我们再回顾一下unsafe值怎么从内存级别操作数据的:

首先要知道你要操作对象的内存地址,也就是AtomicInteger对象引用指向的内存地址

其次是要知道value属性在对象内部的偏移量offset,就可以通过(对象地址 + offset偏移量)直接找到value变量在内存的地址是多少,然后就可以直接给这块内存赋值了。

可以看到AtomicInteger内部其实就是一个 volatile int value的属性、一个unsafe类、一个偏移地址就完事了,Atomic原子类就是对基础的类型进行了一下包装而已,使得他们是线程安全的。比如AtomicInteger要对int进行包装,它内部是有一个属性来存储int的值的。至于它其他两个属性valueOffset、unsafe是辅助实现并发安全的属性。

3.1  AtomicInteger的构造方法

我们再来看看AtomicInteger的构造方法源码:

public AtomicInteger(int initialValue) {
    value = initialValue;
}

public AtomicInteger() {
}

提供了两个构造方法,第一个是在创建AtomicInteger对象的时候直接给内存存储值的volatile int value设置初始化的值
第二个没有赋初始值,那默认就是0;

3.2  AtomicInteger方法的源码分析

3.2.1  getAndIncrement()方法源码

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

我们看到AtomicInteger的getAndIncrement()方法源码很简单,底层就是基于unsafe.getAndAddInt包装了一下,让我们继续看一下unsafe.getAndAddInt方法源码:

public final int getAndAddInt(Object o, long valueOffset, int x) {
    int expected;
    do {
        expected = this.getIntVolatile(o, valueOffset);
    } while(!this.compareAndSwapInt(o, valueOffset, expected, expected + x));
      
    return expected;
}

(1)首先(o + valueOffset)得到value变量在内存中的地址,然后根据地址直接取出value在主内存值,这个值记录为expected
(2)根据 (o + offsetSet)地址偏移量,expected期待的值跟当前内存的值进行对比,如果相等则CAS操作成功内存的值修改为 expected + x
(3)如果值不相等,则进入下一次循环,直到CAS操作成功为止。
(4)由于使用了volatile 修饰符修饰了value,所以一旦修改了别的线程能立马可见、同时volatile还是用内存屏障确保有序性
(5)所以上面的CAS操作确保了原子性,通过volatile确保可见性、有序性;线程安全的三个特性都满足了,上面的操作就是线程安全的。

3.2.2  compareAndSet()方法源码

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

底层也还是直接调用unsafe的compareAndSwapInt方法直接去修改,不过这里不同的是,只会执行一次CAS操作,即使失败了也不会重复CAS

3.2.3  其它方法源码

其它的方法,基本都是直接调用unsafe.getAndInt方法:

public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

4  小结

AtomicInteger基本都是调用unsafe的CAS操作确保原子性,然后使用volatile修饰变量,确保可见性和有序性,有理解不对的地方欢迎指正哈。