初识Atomic

场景: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内部的寄存器中。如下图所示:

在这里插入图片描述

  • 线程之间的共享变量存储在主内存(Main Memory)中
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

简单来说就是每个线程之间都有一个私有的内存。在这个例子中,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
  • 拟写入的新值 B

当且仅当 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 赋值。

  • Unsafe 的 objectFieldOffset 方法可以获取成员属性在内存中的地址相对于对象内存地址的偏移量。说得简单点就是找到这个成员变量在内存中的地址,便于后续通过内存地址直接进行操作。

  • valueOffset 其实就是用来定位 value,后续 Unsafe 类可以通过内存地址直接对 value 进行操作。

  • value就是初始化时用于存放实际数值。

再看下例子中使用的方法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 方法的主要逻辑如下:

  • 根据对象 var1 和内存偏移量 var2 来定位内存地址,获取当前地址值
  • 循环通过 CAS 操作更新当前地址值直到更新成功
  • 返回旧值
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容