通过AtomicInteger来理解CAS

介绍

源码分析

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    //JAVA实现CAS算法的类,整个类有关线程安全的操作,都是借助它来实现。
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //变量value的内存首地址的偏移量。
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //存放int值
    private volatile int value;
  • unsafe:java提供的获得对对象内存地址访问的类,它的作用就是在更新操作时提供“比较并替换”的作用。实际上就是AtomicInteger中的一个工具。
  • valueOffset:是用来记录value本身在内存的偏移地址的,这个记录,也主要是为了在更新操作在内存中找到 value 的位置,方便比较。
  • value:用来存储整数的实际变量,这里被声明为 volatile,就是为了保证在更新操作时,当前线程可以拿到value 最新的值。

valueOffset 详解

在不了解一个事物的时候,最直接的是先看它的作用,然后参考注释或者其他资料,进一步用自己的理解抽象出一个概念。

作用:是用来记录 value 本身在内存的偏移地址的,这个记录,也主要是为了在更新操作在内存中找到 value 的位置,方便比较。

也就是说,valueOffset 的核心作用是用来定位 value 在 AtomicInteger 对象中的位置的。

进一步了解:

valueOffset 是一个静态常量,并且在类加载时就被赋值了。那么这个类在编译后的字节码是一定的,但是在堆中存放 AtomicInteger 对象的首地址是随机的,所以这里的偏移应该是相对于对象实例的首地址。

再通过下图进行辅助理解:


AtomicInteger 内存情况

结论:

  • valueOffset 可以定位 AtomicInteger 中 value 的内存地址
  • AtomicInteger 中 valueOffset 是固定的,因为 valueOffset 是静态变量,AtomicInteger.class 一加载到 JVM 中 valueOffset 就已经生成了
public class AtomicTest {

    public static void main(String[] args) {
        AtomicInteger a = new AtomicInteger(1);
        AtomicInteger b = new AtomicInteger(100);
        int c = a.addAndGet(5);
        b.addAndGet(4);
        System.out.println(a.get());
        System.out.println(c);
    }
}

通过 debug 可以发现:AtomicInteger 中 valueOffset 的值为 12


valueOffset 值

getAndInt

getAndAdd 方法:
/**
     * 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) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

我们已经知道了 valueOffset 的意义,那么 getAndAdd() 方法也比较好理解了

  • this:当前对象
  • valueOffSet:value 在 AtomicInteger 实例中的内存偏移地址
  • delta:希望被加上的数字

valueOffSet 在这里我们可以理解为 value,因为通过 this 和 valueOffSet,我们就可以得到内存中 value 的值。

unsafe 中的 getAndAddInt 方法:

这里将源码中的参数语意化一点,因为源码中直接看变量名不太好理解。这里再把 native 方法 compareAndSwapInt 一并放进来,并进行语义化注释。

    /**
    * 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
    * 
    * @param obj 需要更新的对象
    * @param offset obj中整型field的偏移量
    * @param expect 希望field中存在的值
    * @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
    * @return 如果field的值被更改返回true
    */
    public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
    
    /**
    * unsafe.getAndAddInt
    * 
    * @param atomicInstance 需要更新的对象
    * @param valueOffSet atomicInstance中 value 的偏移量
    * @param delta 希望设置 value 的值为这个新值
    * @return 如果field的值被更改返回true
    */
    public final int getAndAddInt(Object atomicInstance, long valueOffSet, int delta) {
        int result;
        do {
            result = this.getIntVolatile(atomicInstance, valueOffSet);
        } while(!this.compareAndSwapInt(atomicInstance, valueOffSet, result, result + delta));

        return result;
    }

这里就是 CAS 的核心,它会原子性的完成以下三个操作:

  • 获取obj对象中为offset的偏移值,这里假设为realVal
  • 比较realVal和expect
  • 如果相同,将该值更新为update,否则不更新

语义化后的参数我们可以明白:

  1. 会去获取内存中 atomicInstance 中 value 的原始值
  2. 确认原始值没有被其它线程修改时,再执行更新 result + delta 操作,否则会一直循环,等待工作内存中的值与主内存中的值同步

多线程下的 CAS

我们再来对照着源码,走一遍过程,了解一下多线程下 CAS 是如何运作的。

    public final int getAndAddInt(Object atomicInstance, long valueOffSet, int delta) {
        int result;
        do {
            //参考 volatile 的作用,此时获取到的是内存中最新的值
            result = this.getIntVolatile(atomicInstance, valueOffSet);
            //进行CAS对比和更新的原子操作
        } while(!this.compareAndSwapInt(atomicInstance, valueOffSet, result, result + delta));

        return result;
    }

假设现在有两条线程,线程A和线程B在运行AtomicInteger 从1自增到100的过程,假设线程A 先执行。

  1. 线程A 首先执行 getIntVolatile() 方法,由于 getIntVolatile() 获取的值一定是内存中最新的值,所以值为 1。
  2. 线程A 下一步需要做CAS操作,假如此时线程A阻塞了(因为getAndAddInt() 并不是原子性的),线程B开始执行。
  3. 线程B 首先执行 getIntVolatile() 方法,获取到内存中的值同样为1。
  4. 线程B 进行 CAS 操作,从 atomicInstance 中获取到内存中 value 的值为1,与 result 的值相等,所以进行更新,更新的值为 1 + 1 = 2,此时内存中的值为2。线程B 执行结束。
  5. 此时线程A 恢复,继续执行,当进行 CAS 操作时,发现 atomicInstance 中获取到内存中 value 的值为2,而线程A中 result 的值为1,两者不相等,所以不进行更新操作,进行循环。
  6. 下一轮循环,线程A 重新执行 getIntVolatile() 方法,此时获取到的 result 值为 2。
  7. 此时线程A 执行 CAS 操作,从 atomicInstance 中获取到内存中 value 的值为2,与 result 的值相等,所以进行更新,更新的值为 2 + 1 = 3,此时内存中的值为3。线程A 执行结束。

ps:上述内容有些可能描述不清楚,还望参考以下博客进行理解。如描述有错误,还望留言,我会虚心积极改正,谢谢~

参考:
https://www.imooc.com/article/25026?block_id=tuijian_wz
https://www.cnblogs.com/sharkli/p/5623524.html
https://www.jb51.net/article/136718.htm

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