• Java并发杂谈(一):volatile的底层原理,从字节码到CPU


    volatile的特性

    volatile是Java中用于修饰变量的关键字,其主要是保证了该变量的可见性以及顺序性,但是没有保证原子性;其是Java中最为轻量级的同步关键字;
    接下来我将会一步步来分析volatile关键字是如何在Java代码层面、字节码层面、JVM源码层次、汇编层面、操作系统层面、CPU层面来保证可见性和顺序性的;

    Java代码层面

    当一个变量被定义为volatile之后,具备两项特性:

    1. 保证此变量对所有线程的可见性
    2. 禁止指令重排序优化

    volatile所保证的可见性

    volatile所修饰的变量在一条线程修改一个变量的值的时候,新值对于其他线程来说是可以立即知道的;
    普通变量的值在线程间传递的时候都是通过主内存去完成;

    根据JMM我们可以知道,每一个线程其实都有它单独的栈空间,而实际的对象其实都是存放在主内存中的,所以如果是普通对象的话,便会有一个栈空间的对象主内存中的对象存在差异的时间;而volatile所修饰的变量其保持了可见性,其会强制让栈空间所存在的对应变量失效,然后从主内存强制刷新到栈空间,如此便每次看到的都是最新的数据;

    volatile所保证的禁止指令重排

    Java的每一行语句其实都对应了一行或者多行字节码语句,而每一行字节码语句又对应了一行或者多行汇编语句,而每一行汇编语句又对应了一行或者多行机器指令;但是CPU的指令优化器可能会对其指令顺序进行重排,优化其运行效率,但是这样也可能会导致并发问题;而volatile便可以强制禁止优化指令重排;

    volatile在字节码层面的运用

    我们先看到以下代码

    点击查看代码
    public class Main {
            static int a ;
            static volatile int b ;
            public static synchronized void change(int num) {
                    num = 0;
            }
    
            public static void main(String[] args) {
                    a = 10;
                    b = 20;
                    change(a);
            }
    }
    
    

    我们先试用javac来将java文件编译为class文件,然后通过javap -v来反编译;

    点击查看代码
    Classfile /opt/software/java-study/Main.class
      Last modified Mar 1, 2022; size 400 bytes
      MD5 checksum c7691713c9365588495a60da768c32a6
      Compiled from "Main.java"
    public class Main
      SourceFile: "Main.java"
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#20         //  java/lang/Object."<init>":()V
       #2 = Fieldref           #5.#21         //  Main.a:I
       #3 = Fieldref           #5.#22         //  Main.b:I
       #4 = Methodref          #5.#23         //  Main.change:(I)V
       #5 = Class              #24            //  Main
       #6 = Class              #25            //  java/lang/Object
       #7 = Utf8               a
       #8 = Utf8               I
       #9 = Utf8               b
      #10 = Utf8               <init>
      #11 = Utf8               ()V
      #12 = Utf8               Code
      #13 = Utf8               LineNumberTable
      #14 = Utf8               change
      #15 = Utf8               (I)V
      #16 = Utf8               main
      #17 = Utf8               ([Ljava/lang/String;)V
      #18 = Utf8               SourceFile
      #19 = Utf8               Main.java
      #20 = NameAndType        #10:#11        //  "<init>":()V
      #21 = NameAndType        #7:#8          //  a:I
      #22 = NameAndType        #9:#8          //  b:I
      #23 = NameAndType        #14:#15        //  change:(I)V
      #24 = Utf8               Main
      #25 = Utf8               java/lang/Object
    {
      static int a;
        flags: ACC_STATIC
    
      static volatile int b;
        flags: ACC_STATIC, ACC_VOLATILE
    
      public Main();
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0       
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return        
          LineNumberTable:
            line 1: 0
    
      public static synchronized void change(int);
        flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
        Code:
          stack=1, locals=1, args_size=1
             0: iconst_0      
             1: istore_0      
             2: return        
          LineNumberTable:
            line 5: 0
            line 6: 2
    
      public static void main(java.lang.String[]);
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=1, locals=1, args_size=1
             0: bipush        10
             2: putstatic     #2                  // Field a:I
             5: bipush        20
             7: putstatic     #3                  // Field b:I
            10: getstatic     #2                  // Field a:I
            13: invokestatic  #4                  // Method change:(I)V
            16: return        
          LineNumberTable:
            line 9: 0
            line 10: 5
            line 11: 10
            line 12: 16
    }
    
    
    我们仔细观察加了volatile修饰的变量与其他变量的区别便可以看出,其主要是在flags中添加了一个**ACC_VOLATILE**;同时先进行**putstatic**指令;

    volatile在JVM源码方面的运用

    在JVM源码方面,我编译了OpenJDK7然后利用find与grep进行全局查找,然后进行方法追踪,由于涉及到大量C++的知识,我便跳过其C++代码追踪,而直接看最后追踪到的函数;

    先来做一个总结,其实volatile的JVM源码的原理对应的是被称为内存屏障来实现的;

    点击查看代码
    static void     loadload();
    static void     storestore();
    static void     loadstore();
    static void     storeload();
    
    这四个分别对应了经常在书中看到的JSR规范中的读写屏障
    • LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

    对于volatile操作而言,其操作步骤如下:

    • 每个volatile写入之前,插入一个StoreStore,写入以后插入一个StoreLoad
    • 每个volatile读取之前,插入一个LoadLoad,读取之后插入一个LoadStore

    在JVM源码层次而言,内存屏障直接起到了禁止指令重排的作用,且之后与总线锁或者MESI协议配合实现了可见性;

    汇编层次

    在汇编层次而言,我是使用JITWatch配合hsdis进行的转汇编,可以发现在含有volatile的变量的时候,汇编指令会有一个lock前缀,而lock前缀在CPU层次中自己实现了内存屏障的功能;

    CPU层次

    在x86的架构中,含有lock前缀的指令拥有两种方法实现;
    一种是开销很大的总线锁,它会把对应的总线直接全部锁住,如此明显是不合理的;
    所以后期intel引入了缓存锁以及mesi协议,如此便可以轻量化的实现内存屏障;

  • 相关阅读:
    一文读懂TDengine的窗口查询功能
    Java 使用 poi 和 aspose 实现 word 模板数据写入并转换 pdf 增加水印
    EPLAN_007#3D图形的导入、编辑和定义
    网络监控应用程序
    简单聊聊ThreadLocal吧
    前端技能树初体验
    9、Gc(复制算法)
    ES6——知识点记录
    Visual Studio2022 离线安装包下载
    Kotlin高仿微信-第22篇-个人信息-修改昵称
  • 原文地址:https://www.cnblogs.com/hyx-hasaki/p/15950745.html