JUC包-原子类(AtomicInteger为例)

时间:2024-01-27 21:20:32

JUC包-原子类

为什么需要JUC包中的原子类

首先,一个简单的i++可以分为三步:

  1. 读取i的值

  2. 计算i+1

  3. 将计算出i+1赋给i

这就无法保证i++的原子性,即在i++过程中,可能会出现其他线程也读取了i的

值,但读取到的不是更改过后的i的值。

原子类原理(AtomicInteger为例)

原子类的原子性是通过volatile + CAS实现原子操作的。

volatile

AtomicInteger源代码

AtomicInteger类中的value是有volatile关键字修饰的,这就保证了value的内存可见性,这为后续的CAS实现提供了基础。

CAS

通过查看源码可以发现,AtomicInteger类的值更新操作都是通过调用

getAndAddInt(Object var1, long var2, int var4)方法实现

/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
    //返回的是修改前的值,类似于i++
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    //返回的是更新后的值,类似于++i
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

当我们查看getAndAddInt方法的具体实现,可以发现在整个方法中存在一个循

环,这就是我们说的自旋锁,顾名思义,while语句里面的条件一直为true,这个

循环就会一直执行下去。

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;
    }

下面我们来分析getAndAddInt方法中的各个参数的具体含义:

Object var1:this,表示当前对象

long var2:valueOffset,表示当前对象的内存偏移量

int var4:delat,需要加上的数值

所以整个方法的运行流程可以归纳为:

  1. 读取传入对象this在主存中偏移量为offset位置的值赋值给var5

  2. 将var5的值与当前线程对象内存中偏移量为offset位置的值进行比较(compare)

  3. 如果相等,将var5+var4的值更新到对象内存中偏移量为offset位置(swap);如果不

    相等,就进入while循环自旋。

CAS的缺点

  1. 循环时间长,开销大

    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却

    又一直更新不成功,循环往复,会给CPU带来很大的压力

  2. 只能保证一个共享变量的原子性操作

    CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块

    的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用

    Synchronized了

  3. ABA问题

    见下文

ABA问题

什么是ABA问题

简单来说就是CAS过程只在乎当前值期望值是否相等,只在乎最终结果,不考虑中

间变化,具体可以看下面一个简单的例子。

public class Test {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) {
        atomicInteger.compareAndSet(0,1);
        System.out.println("线程A第一次修改:0->" + atomicInteger.get());
        new Thread(() -> {
            atomicInteger.compareAndSet(1,0);
            System.out.println("线程A第二次修改:1->" + atomicInteger.get());
        }, "testA").start();

        new Thread(() -> {
            try {
                //确保A线程修改完毕
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicInteger.compareAndSet(0,2);
            System.out.println("线程B第一次修改:0->" + atomicInteger.get());
        }, "testB").start();
    }
}

程序运行后输出的结果,由此可见AtomicInteger的CAS中间步骤有变化,但是没有被感知到。

ABA问题

ABA问题的解决办法

一个简单的想法是,在数据上加上时间戳(版本号),使得线程每次对变量进行修改时,不仅要对比值,还要

对比时间戳(版本号),每次修改操作都会导致时间戳(版本号)改变为新的

值;

我们通过AtomicStampedReference类引入版本号,如下图所示

public class Test {
    //初始化数值为0,版本号为1
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(0, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            /* compareAndSet四个参数分别为 
             * 期望值/新的值/期望版本号/新的版本号
             */
            atomicStampedReference.compareAndSet(0, 1, 1, 2);
            System.out.println("数值第一次修改为" + atomicStampedReference.getReference() +
                    " 版本号第一次修改为" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(1, 0, 2, 3);
            System.out.println("数值第二次修改为" + atomicStampedReference.getReference() +
                    " 版本号第二次修改为" + atomicStampedReference.getStamp());
        }, "testA").start();

        new Thread(() -> {
            try {
                //确保A线程修改完毕
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(0, 1, 1, 2);
            System.out.println("数值第三次修改为" + atomicStampedReference.getReference() +
                        " 版本号第三次修改为" + atomicStampedReference.getStamp());
        }, "testB").start();
    }
}

上述代码的程序运行结果如下图所示,可以看到当第三次修改的时候,虽然期望值0匹配,但是期望版本号不匹配,导致第三次修改无效。