• Volatile从浅到深


    目录

    一、Volatile可见性问题

    二、Idea查看字节码

    三、查看汇编代码

    四、Volatile 与内存屏障 Memory Barrier

    大神 Doug Lea 写的一篇Java内存屏障说明:


    一、Volatile可见性问题

    1. public class TestVolatileVisibility {
    2. public static /* volatile */int found = 0;
    3. public static void main(String[] args) {
    4. new Thread(() -> {
    5. System.out.println("A waiting money,Begin...");
    6. // 加上 volatile 能让每次都去内存读取数据到CPU cache
    7. while (0 == found){
    8. }
    9. System.out.println("A get the money, End ...");
    10. },"myThread-A").start();
    11. new Thread(() -> {
    12. try {
    13. Thread.sleep(2000);
    14. } catch (Exception e){
    15. e.printStackTrace();
    16. }
    17. System.out.println("B send money");
    18. change();
    19. },"myThread-B").start();
    20. }
    21. public static void change(){
    22. found = 1;
    23. }
    24. }

    输出:

    1. A waiting money,Begin...
    2. B send money
    3. A get the money, End ...

    如果不加关键字 volatile,那么A线程将一直等待,因为A线程的found一直是去cpu缓存中读取,不会去内存中重新刷出来。从而导致一直 while循环。

    volatile保证了可见性,能保证每次都能去内存读,而不单单取CPU缓存中的数据。

    二、Idea查看字节码

    下载插件: jclasslib Bytecode viewer

    或者直接 javap -v -p class文件名(不要.class 后缀)运行

    IDEA -- Reference -- Plugins

    先Build一下,生成Class文件

    View -- Show Bytecode With Jclasslib

    可以发现,加了 Volatile的时候,从字节码中,可以看到 Access flags中是有 volatile,

    自己去比对就知道,java代码中,其他地方都没区别,加不加 volatile,在字节码层面只在 Access flags这个地方有区别。

    三、查看汇编代码

    Mac下查看已安装的jdk版本及其安装目录

    1. localhost:bin jeblin$ /usr/libexec/java_home -V
    2. Matching Java Virtual Machines (1):
    3. 1.8.0_231, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home
    4. /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home

    工具 : hsdis-amd64.dylib

        将上述两个文件放在你的 jre/bin 路径下的路径里。

     /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/hsdis-amd64.dylib

    idea配置 VM options:

    -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatileVisibility.change

    这块(TestVolatileVisibility.change)换成你的类名+方法名字

     然后运行,或者直接命令行运行Java

    java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite 包名.类名

     

    1. 0x00000001128318ad: lock addl $0x0,(%rsp) ;*putstatic found
    2. ; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)

    如果没有Volatile关键字,那么就没有 lock

    1. 0x0000000109ab3565: movl $0x1,0x68(%rsi) ;*putstatic found
    2. ; - indi.sword.util.basic.Thread.Volatile.TestVolatileVisibility::change@1 (line 31)

    可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?

    lock是一种控制指令,在多处理器环境下,lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果。

    查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。(我的理解是通知shared状态的cpu强制去消费invalid queue队列,把位于cpu Cache中的共享的变量 CacheLine置为Invalid,然后重新去内存中读取最新的值)所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。

    也就是Volatile解决了可见性的问题

    所以,它的作用是

    1. 锁住主存
    2. 任何读必须在写完成之后再执行
    3. 使其它线程这个值的栈缓存失效

    类似于前面是storestore,后面是storeload

    四、Volatile 与内存屏障 Memory Barrier

    Volatile使用内存屏障,内存屏障的作用,是为了阻止各阶段的乱序优化

    Java内存模型关于重排序的规定,总结后如下表所示:

    表格中的“是否允许重排序”,意思是指,第一项操作和第二项操作会不会被重排序,也就是这俩操作会不会被乱序优化

    大神 Doug Lea 写的一篇Java内存屏障说明:

    The JSR-133 Cookbook

    竖的是第一个操作,横的是第二个操作。

    LoadLoad是啥含义? 左边的Load表示第一个操作是读,右边的Load表示第二个操作是读。其他以此类推。

    Required barriers2nd operation
    1st operationNormal LoadNormal StoreVolatile Load
    MonitorEnter
    Volatile Store
    MonitorExit
    Normal LoadLoadStore
    Normal StoreStoreStore
    Volatile Load
    MonitorEnter
    LoadLoadLoadStoreLoadLoadLoadStore
    Volatile Store
    MonitorExit
    StoreLoadStoreStore

    Here's an example showing placements.

    JavaInstructions
    class X {
      int a, b;
      volatile int v, u;
      void f() {
        int i, j;
       
        i = a;
        j = b;
        i = v;
       
        j = u;
       
        a = i;
        b = j;
       
        v = i;
       
        u = j;
       
        i = u;
       
       
        j = b;
        a = i;
      }
    }
         






    load a
    load b
    load v
       LoadLoad
    load u
       LoadStore
    store a
    store b
       StoreStore
    store v
       StoreStore
    store u
       StoreLoad
    load u
       LoadLoad
       LoadStore
    load b
    store a

    从上表我们可以看出:

    1. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。(也就是Volatile 读完之后,才去干后面的事情)
    2. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。(也就是Volatile写之前,前面事情已经干完了)
    3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。(写的第2条已经说了,不能)

  • 相关阅读:
    什么是数字化管理?产业园区如何进行数字化管理?
    Java-多线程
    Redis 过期淘汰机制
    编译器一日一练(DIY系列之词法分析)
    RabbitMQ 如何实现延迟队列?
    2022 极术通讯-《服务器应用场景性能测试方法 虚拟化》解读
    HTML5播放 M3U8的hls流地址
    [找规律]Number Game 2022牛客多校第6场 J
    什么野指针(c++)
    【Linux】《Linux命令行与shell脚本编程大全 (第4版) 》笔记-ChapterB-sed 和 gawk 快速指南
  • 原文地址:https://blog.csdn.net/Sword52888/article/details/126059083