Java原子类中CAS的底层实现

Java原子类中CAS的底层实现 - GoldArowana - 博客园

Java原子类中CAS的底层实现

从Java到c++到汇编, 深入讲解cas的底层原理.

介绍原理前, 先来一个Demo

以AtomicBoolean类为例.先来一个调用cas的demo.

主线程在for语句里cas忙循环, 直到cas操作成功返回true为止.

而新开的一个县城new Thread 会在4秒后,将flag设置为true, 为了让主线程能够设置成功.(因为cas的预期值是true, 而flag被初始化为了false)

现象就是主线程一直在跑for循环. 4秒后, 主线程将会设置成功, 然后输出时间差, 然后终止for循环.

public class TestAtomicBoolean {

    public static void main(String[] args) {

        AtomicBoolean flag = new AtomicBoolean(false);


        long start = System.currentTimeMillis();


        new Thread(()->{

            try {

                Thread.sleep(4000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            flag.set(true);

        }).start();


        for(;;){

            if(flag.compareAndSet(true,false)){

                System.out.println(System.currentTimeMillis() - start);

                System.out.println("inner loop OK!");

                break;

            }

        }

    }

}

这里只是举了一个例子, 也许这个例子也不太恰当, 本文只是列出了这个api的调用方法而已, 重点在于介绍compareAndSet()方法的底层原理.

Java级源码AtomicBoolean.java

发现AtomicBoolean的compareAndSet()调用的是unsafe里的compareAndSwapInt()方法.

Java级源码Unsafe.java

有的同学可能好奇, 其中的unsafe是怎么来的.

在AtomicBoolean类中的静态成员变量:

如果还要细究Unsafe.getUnsafe()是怎么实现的话....那么我再贴一份Unsafe类里的getUnsafe的代码:

首先, 在Unsafe类里, 自己就有了一个自己的实例.(而且是单例的)

然后Unsafe类里的getUnsafe()方法会进行检查, 最终会return这个单例 theUnsafe.

刚刚跑去取介绍了getUnsafe()方法...接下来继续讲解cas...

刚才说到了AtomicBoolean类里的compareAndSet()方法内部其实调用了Unsafe类里的compareAndSwapInt()方法.

Unsafe类里的compareAndSwapInt源码如下:

(OpenJDK8的源码里路径: openjdk/jdk/src/share/classes/sun/misc/Unsafe.java)

发现这里是一段native方法.说明继续看源码的话, 从这里就开始脱离Java语言了....

c++级源码Unsafe.cpp

本源码在OpenJDK8里的路径为: openjdk/hotspot/src/share/vm/prims/unsafe.cpp

(这里临时跑题一下:   如果说要细究 UNSAFE_ENTRY 是什么的话...UNSAFE_ENTRY 就是 JVM_ENTRY, 而 JVM_ENTRY 在interfaceSupport.hpp里面定义了, jni相关.如果想看的话, 源码路径在OpenJDK8中的路径是这个: 

openjdk/hotspot/src/share/vm/runtime/interfaceSupport.hpp)

回到本文的主题cas....上面截图的这段代码, 看后面那一句return, 发现其中的使用到的是Atomic下的cmpxchg()方法.

c++级源码atomic.cpp

本段源码对应OpenJDK8的路径是这个: openjdk/hotspot/src/share/vm/runtime/atomic.cpp

其中的cmpxchg为核心内容. 但是这句代码根据操作系统和处理器的不同, 使用不同的底层代码. 

而atomic.inline.hpp里声明如下:

可见 ...不同不同操作系统, 不同的处理器, 都要走不同的cmpxchg()方法的实现.

咱们接下来以其中的linux操作系统 x86处理器为例 , atomic_linux_x86.inline.hpp

汇编级源码atomic_linux_x86.inline.hpp

OpenJDK中路径如下: openjdk/hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

看到了__asm__, 说明c++要开始内联汇编了,说明继续看代码的话, 将会是汇编语言.

这是一段内联汇编:

其中 __asm__ volatile 指示了编译器不要改动优化后面的汇编语句, 如果进行了优化(优化是为了减少访问内存, 直接通过缓存, 加快取读速度), 那么就在这段函数的周期内, 某几个变量就相当于常亮了, 其值可能会与内存中真实的值有差异.

2018.6.7更新: 10天没更新了, 由于实习结束, 这几天手头没电脑, 而且租房火车回学校等各种问题繁杂...

了解汇编指令cmpxchg

环境

怼代码之前...先把环境介绍一下...汇编的指令细节方便会有所不同, 比如__asm__  和asm不一定一样, 又比如 asm(...)  和  asm{...}  的括号问题, 又比如 汇编指令的首操作数和第2操作数的含义是颠倒的, 也就是`mov 操作数1, 操作数2` , 要改写成`mov  操作数2, 操作数1`...反正很乱..我也不专门搞汇编, 我只能保证我这里使用g++进行编译能正常运行.(linux下或者mac下用g++编译是没问题的, windows就不知道了...)

内联汇编格式

有些同学可能会对上面突如其来的汇编语句感到迷茫...

先来一下内联汇编的语法格式:

asm volatile("Instruction List"


: Output


: Input


: Clobber/Modify);

其中的Output和Input大家很好理解, 但是Clobber/Modify是什么意思呢:

(下面关于Clobber/Modify的内容引用自:https://blog.csdn.net/dlh0313/article/details/52172833)

有时候,当你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望GCC在编译时能够将这一点考虑进去;那么你就可以在Clobber/Modify部分声明这些寄存器或内存

大家可以看看上面图片中openJDK中的汇编代码, 里面的Clobber/Modify部分是"cc"和"memory", memory好理解, 就是内存. 那么"cc"是什么呢?

当一个内联汇编中包含影响标志寄存器eflags的条件,那么也需要在Clobber/Modify部分中使用"cc"来向GCC声明这一点

内联汇编的简单例子

接下来展示一个简单的c++内联汇编的程序: a是1000, 通过汇编来给b变量也赋值为1000

#include<iostream>


using namespace std;


int main() {

    int a = 1000, b = 0;


    asm("movl %1,%%eax\n"

        "movl %%eax,%0\n"

    : "=r"(b)

    : "r" (a)

    : "%eax");

    cout << "a := " << a << endl;

    cout << "b := " << b << endl;

    return 0;

}

首先在c++里定义了a=1000, b=0;

然后看asm里的第一行冒号, 这里表示Output    ` : "=r"(b) `, b是第0个参数, 等号(=)表示当前输出表达式的属性为只写, r表示寄存器

然后看asm里的第一行冒号, 这里表示Input        `: "r" (a)`, a是第1个参数, r表示寄存器.

然后看asm里的第一行指令   `movl %1,%%eax\n`     , 将第0个参数的值(变量a)传入到寄存器eax中

然后看asm里的第二行指令   `movl %%eax,%0\n`     , 将寄存器eax的值传给第一个参数(变量b)

接下来使用c++的cout进行输出, 查看是否赋值成功, 结果如下:

再来一个简单汇编例子

用汇编给a变量赋值为100

#include<iostream>


using namespace std;


int main() {

    int a = 0;


    asm("movl $100,%%eax\n"

        "movl %%eax,%0\n"

    : "=r"(a)

    : /*no input*/

    : "%eax", "memory");

    cout << "a := " << a << endl;

    return 0;

}

(当然如果用的熟练, 就不需要使用两句mov来实现这个功能. 可以直接 movl $100, %0 ,就可以实现把100赋值给a变量了. 这里只是为了演示使用寄存器, 所以先给寄存器eax存上100, 再把寄存器的值赋值给a, 用寄存器eax做了一个中间的临时存储)

程序运行结果如下:

内联汇编cmpxchg(cmpxchg比对成功)

(有同学不会编译运行g++下的cpp, 我顺便也在这里做一个示范吧.....)

#include<iostream>


using namespace std;


int main() {

    int cpp_eax = 0;

    int cpp_ebx = 0;

    int expect = 2222;

    int target = 8888;

    asm("movl    %2,    %%eax \n" /*将2222存入到eax寄存器中*/

        "movl    %3,    %%ebx \n" /*将8888存入到ebx寄存器中*/

        "cmpxchg %%ebx, %2    \n" /*如果变量expect的值与寄存器eax的值相等(成功), 那么ebx的值就赋给expect*/

        "movl    %%eax, %0    \n" /*将寄存器eax的值赋值给变量cpp_eax*/

        "movl    %%ebx, %1    \n" /*将寄存器ebx的值赋值给变量cpp_ebx*/

    :"=r"(cpp_eax), "=r"(cpp_ebx), "+r"(expect) /*等于号表示可写, 加号表示可写可读*/

    :"r"(target)

    /*cpp_eax是%0, cpp_ebx是%1, expect是%2, target是%3*/

    :"%eax", "%ebx", "memory", "cc");

    cout << "eax := " << cpp_eax << endl;

    cout << "ebx := " << cpp_ebx << endl;

    cout << "expect := " << expect << endl;

    cout << "target := " << target << endl;

}

 运行方式和结果如下:

内联汇编cmpxchg(cmpxchg比对失败)

如果cmpxchg指令中的第二个操作数与寄存器eax进行比对, 发现值不一样的时候, 就会比对失败, 那么expect的值就会赋值给eax, 而expect的值保持不变.


#include<iostream>


using namespace std;


int main() {

    int cpp_eax = 0;

    int cpp_ebx = 0;

    int expect = 2222;

    int target = 8888;

    asm("movl    $77,   %%eax \n" /*将字面量77存入到eax寄存器中*/

        "movl    %3,    %%ebx \n" /*将8888存入到ebx寄存器中*/

        "cmpxchg %%ebx, %2    \n" /*如果变量expect的值与寄存器eax的值不相等(失败), 那么expect的值就赋给eax*/

        "movl    %%eax, %0    \n" /*将寄存器eax的值赋值给变量cpp_eax*/

        "movl    %%ebx, %1    \n" /*将寄存器ebx的值赋值给变量cpp_ebx*/

    :"=r"(cpp_eax), "=r"(cpp_ebx), "+r"(expect) /*等于号表示可写, 加号表示可写可读*/

    :"r"(target)

    /*cpp_eax是%0, cpp_ebx是%1, expect是%2, target是%3*/

    :"%eax", "%ebx", "memory", "cc");

    cout << "eax := " << cpp_eax << endl;

    cout << "ebx := " << cpp_ebx << endl;

    cout << "expect := " << expect << endl;

    cout << "target := " << target << endl;

}

 cmpxchg指令结论

     1.指令格式: CMPXCHG 操作数1 (8位/16位/32位寄存器),  操作数2(可以是任意寄存器或者内存memory)


     2.指令作用: 将累加器AL/AX/EAX(也就是%eax)中的值与第2操作数(目的操作数)比较,如果相等,第首操作数(源操作数)的值装载到第2操作数,zf置1。如果不等, 第2操作数的值装载到AL/AX/EAX并将zf清0


     3.该指令只能用于486及其后继机型。

lock前缀

本段的文字理论介绍引用自(http://www.weixianmanbu.com/article/736.html):

 在单处理器系统中是不需要加lock的,因为能够在单条指令中完成的操作都可以认为是原子操作,中断只能发生在指令与指令之间。

 在多处理器系统中,由于系统中有多个处理器在独立的运行,即使在能单条指令中完成的操作也可能受到干扰。

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

#include<iostream>


using namespace std;


int main() {

    int cpp_eax = 0;

    int cpp_ebx = 0;

    int expect = 2222;

    int target = 8888;


    __asm__ __volatile__("movl    $2222, %%eax \n" /*将字面量2222存入到eax寄存器中*/

                         "movl    %3   , %%ebx \n" /*将8888存入到ebx寄存器中*/

                 "lock;" "cmpxchg %%ebx, %2    \n" /*如果变量expect的值与寄存器eax的值不相等(失败), 那么expect的值就赋给eax*/

                         "movl    %%eax, %0    \n" /*将寄存器eax的值赋值给变量cpp_eax*/

                         "movl    %%ebx, %1    \n" /*将寄存器ebx的值赋值给变量cpp_ebx*/

    :"=r"(cpp_eax), "=r"(cpp_ebx), "=m"(expect) /*等于号表示可写, 加号表示可写可读*/

    :"r"(target)

    /*cpp_eax是%0, cpp_ebx是%1, expect是%2, target是%3*/

    :"%eax", "%ebx", "memory", "cc");

    cout << "eax := " << cpp_eax << endl;

    cout << "ebx := " << cpp_ebx << endl;

    cout << "expect := " << expect << endl;

    cout << "target := " << target << endl;

}

 结果如下:

注意, 如果加lock前缀的话, 指令的第2操作数必须是存储在内存中的, 语句格式是这样`  lock; cmpxchg 操作数1(寄存器), 操作数2(内存) ` .所以, expect作为output的时候是这样声明的: "=m"(expect)

 如果不是操作的内存, 那么会报异常. 操作数2必须是内存...在这里卡了很久

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

推荐阅读更多精彩内容