初识Atomic

2020-12-17  本文已影响0人  nitricoxide

场景:i++是线程安全的吗?

首先看段代码:

public class Test {

    static Integer num = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
               for (int j = 0; j < 100; j++) {
                   num++;
               }
            });
            thread.start();
        }
        System.out.println(num);
    }
}

100个线程同时操作全局变量num,每个线程都对num进行100次循环的++操作,理论上最后的结果是10000,实际上却不是。

输出结果:

7385

原因:
Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:

在这里插入图片描述

简单来说就是每个线程之间都有一个私有的内存。在这个例子中,0线程读取了之后,++操作可能在私有的内存中进行,并不会写入主存,此时1线程开启,读取的还是主内存中的值。

参考来源及推荐阅读:Java内存模型(JMM)总结

解决方案

  1. 对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作(效率慢);
  2. 将num变成局部变量(可能不满足部分需求);
  3. 使用Atomic原子类(效率比1要高);

有些同学可能了解过volatile关键字,那么在这个场景中==volatile关键字并不能起到作用==。

先了解一下volatile的作用:

加上这个关键字之后,它会强制将对缓存的修改操作立即写入主存,如果是写操作,会导致其他CPU中对应的缓存无效。

看一下效果

public class Test {

    static volatile Integer num = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
               for (int j = 0; j < 100; j++) {
                   num++;
               }
            });
            thread.start();
        }
        System.out.println(num);
    }
}

结果:

9022

原因:
假设当前值是100

  1. 0线程读取100,进行++操作(未写入)
  2. 1线程读取100,进行++操作,此时0线程写入主存(此时主存的值是101)
  3. 1线程写入主存(此时主存被1线程写入后还是101)

也就是说,多个线程同时读取这个共享变量的值,就算保证其他线程修改的可见性,也不能保证线程之间读取到同样的值然后相互覆盖对方的值的情况。

这时候AtomicInteger就登场了,先看代码。

public class Test {

    static AtomicInteger num = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
               for (int j = 0; j < 100; j++) {
                   num.incrementAndGet();
               }
            });
            thread.start();
        }
        //主线程运行到这一步的时候,子线程可能还没结束,所以先让主线程休眠一段时间再输出
        System.out.println(num);
        Thread.sleep(1000);
        System.out.println(num);
    }
}

结果:

9900
10000

AtomicInteger

概念

AtomicInteger 是一个 Java concurrent 包提供的一个原子类,通过这个类可以对 Integer 进行一些原子操作。这个类的源码比较简单,主要是依赖于 sun.misc.Unsafe 提供的一些 native 方法保证操作的原子性。使用CAS操作具体实现。

CAS

首先我们来简单理解一下CAS的概念。

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

AtomicInteger源码

先看下AtomicInteger的成员变量和静态代码块

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

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

private volatile int value;

AtomicInteger 主要有两个静态变量和一个成员变量,在初始化的时候会通过静态代码块给 valueOffset 赋值。

再看下例子中使用的方法incrementAndGet()

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

再看下getAndAddInt()方法

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

调用了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;
}

这时候会发现它里面又调用了 getIntVolatile 和 compareAndSwapInt 方法,而这两个方法都是 native 方法,具体说明可以参照 Unsafe 的 API 文档。

getIntVolatile 的主要作用是通过对象 var1 和成员变量相对于对象的内存偏移量 var2 来直接从内存地址中获取成员变量的值,所以 var5 就是当前 AtomicInteger 的值。

compareAndSwapInt的主要逻辑如下:

  1. 通过对象 var1 和成员变量的内存偏移量 var2 来定位内存地址
  2. 判断当前地址的值是否等于 var5
    不等于:返回 false
    等于:把当前地址的值替换成 var5 + var4 并返回 true

所以,综合来说,getAndAddInt 方法的主要逻辑如下:

上一篇 下一篇

猜你喜欢

热点阅读