美文网首页
深入了解volatile关键字

深入了解volatile关键字

作者: 谭嘉俊 | 来源:发表于2020-08-24 02:56 被阅读0次

    本文章已授权微信公众号郭霖(guolin_blog)转载。

    本文章讲解的内容是深入了解volatile关键字,建议对着示例项目阅读文章,示例项目链接如下:

    VolatileDemo

    查看汇编代码hsdis-amd64.dylib文件链接如下:

    hsdis-amd64.dylib

    汇编代码链接如下:

    关键字volatileJava虚拟机提供的最轻量级的同步机制,当一个变量被关键字volatile修饰之后,它有如下两个特性

    • 保证了这个变量对所有线程的可见性
    • 禁止指令重排序优化

    保证变量对所有线程的可见性

    关键字volatile可以保证变量对所有线程的可见性,也就是当一个线程修改了这个变量的值其他线程能够立即得到修改的值普通变量是做不到这样,普通变量的值需要通过主内存线程之间传递,举个例子:线程A修改一个普通变量的值,然后传送给主内存,另外一个线程B需要等到传送完主内存后才能够从主内存进行读取操作,这样变量最新的值才会对线程B可见。

    先看下如下例子,代码如下所示:

    /**
     * Created by TanJiaJun on 2020-08-16.
     */
    class VolatileDemo {
    
        private static final int THREADS_COUNT = 10;
    
        private static volatile int value = 0;
    
        private static void increase() {
            // 对value变量进行自增操作
            value++;
        }
    
        public static void main(String[] args) {
            // 创建10个线程
            Thread[] threads = new Thread[THREADS_COUNT];
            for (int i = 0; i < THREADS_COUNT; i++) {
                threads[i] = new Thread(() -> {
                    for (int j = 0; j < 1000; j++)
                        // 每个线程对value变量进行1000次自增操作
                        increase();
                });
                threads[i].start();
            }
            // 主线程等待子线程运行结束
            for (Thread thread : threads) {
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("value的值:" + value);
        }
    
    }
    

    这段代码的意思是发起10个线程,然后每个线程对value变量进行1000次自增操作,如果这段代码正确地并发操作,最后的结果value的值应该是10000,但是实际上多次运行后,value的值都是小于等于10000的值。

    这段代码中increase方法调用i++,也就是i = i + 1它不是原子性操作Java内存模型直接保证的原子性变量操作包括readloadassignusestorewrite,我们可以认为基本数据类型的读写都具备原子性,有个例外就是longdouble非原子性协定不过我们无须太过在意,虽然Java内存模型允许虚拟机不把longdouble的变量的读写实现为原子性操作,但是现在的商用虚拟机都几乎把这些操作实现为原子性操作原子性操作是指执行一系列操作这些操作要么全部执行要么全部不执行不存在只执行其中一部分的情况,举个例子:i = 1就是个原子性操作,但是i = i + 1就不是原子性操作,因为这个操作是由多条字节码指令构成的,我用Javap反编译上面的示例代码,先找到生成的Class文件,路径是/Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class,就是在VolatileDemo目录下的out文件夹中,然后执行javap -p -v VolatileDemo命令,生成如下字节码

    Classfile /Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class
      Last modified 2020年8月19日; size 2160 bytes
      SHA-256 checksum 41886220d7539d7ff90dfcd4a870539affb765699bcef3cb07119658a4a5d1a3
      Compiled from "VolatileDemo.java"
    class VolatileDemo
      minor version: 0
      major version: 57
      flags: (0x0020) ACC_SUPER
      this_class: #8                          // VolatileDemo
      super_class: #2                         // java/lang/Object
      interfaces: 0, fields: 2, methods: 5, attributes: 3
    Constant pool:
        #1 = Methodref          #2.#3         // java/lang/Object."<init>":()V
        #2 = Class              #4            // java/lang/Object
        #3 = NameAndType        #5:#6         // "<init>":()V
        #4 = Utf8               java/lang/Object
        #5 = Utf8               <init>
        #6 = Utf8               ()V
        #7 = Fieldref           #8.#9         // VolatileDemo.value:I
        #8 = Class              #10           // VolatileDemo
        #9 = NameAndType        #11:#12       // value:I
       #10 = Utf8               VolatileDemo
       #11 = Utf8               value
       #12 = Utf8               I
       #13 = Class              #14           // java/lang/Thread
       #14 = Utf8               java/lang/Thread
       #15 = InvokeDynamic      #0:#16        // #0:run:()Ljava/lang/Runnable;
       #16 = NameAndType        #17:#18       // run:()Ljava/lang/Runnable;
       #17 = Utf8               run
       #18 = Utf8               ()Ljava/lang/Runnable;
       #19 = Methodref          #13.#20       // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
       #20 = NameAndType        #5:#21        // "<init>":(Ljava/lang/Runnable;)V
       #21 = Utf8               (Ljava/lang/Runnable;)V
       #22 = Methodref          #13.#23       // java/lang/Thread.start:()V
       #23 = NameAndType        #24:#6        // start:()V
       #24 = Utf8               start
       #25 = Methodref          #13.#26       // java/lang/Thread.join:()V
       #26 = NameAndType        #27:#6        // join:()V
       #27 = Utf8               join
       #28 = Class              #29           // java/lang/InterruptedException
       #29 = Utf8               java/lang/InterruptedException
       #30 = Methodref          #28.#31       // java/lang/InterruptedException.printStackTrace:()V
       #31 = NameAndType        #32:#6        // printStackTrace:()V
       #32 = Utf8               printStackTrace
       #33 = Fieldref           #34.#35       // java/lang/System.out:Ljava/io/PrintStream;
       #34 = Class              #36           // java/lang/System
       #35 = NameAndType        #37:#38       // out:Ljava/io/PrintStream;
       #36 = Utf8               java/lang/System
       #37 = Utf8               out
       #38 = Utf8               Ljava/io/PrintStream;
       #39 = InvokeDynamic      #1:#40        // #1:makeConcatWithConstants:(I)Ljava/lang/String;
       #40 = NameAndType        #41:#42       // makeConcatWithConstants:(I)Ljava/lang/String;
       #41 = Utf8               makeConcatWithConstants
       #42 = Utf8               (I)Ljava/lang/String;
       #43 = Methodref          #44.#45       // java/io/PrintStream.println:(Ljava/lang/String;)V
       #44 = Class              #46           // java/io/PrintStream
       #45 = NameAndType        #47:#48       // println:(Ljava/lang/String;)V
       #46 = Utf8               java/io/PrintStream
       #47 = Utf8               println
       #48 = Utf8               (Ljava/lang/String;)V
       #49 = Methodref          #8.#50        // VolatileDemo.increase:()V
       #50 = NameAndType        #51:#6        // increase:()V
       #51 = Utf8               increase
       #52 = Utf8               THREADS_COUNT
       #53 = Utf8               ConstantValue
       #54 = Integer            10
       #55 = Utf8               Code
       #56 = Utf8               LineNumberTable
       #57 = Utf8               LocalVariableTable
       #58 = Utf8               this
       #59 = Utf8               LVolatileDemo;
       #60 = Utf8               main
       #61 = Utf8               ([Ljava/lang/String;)V
       #62 = Utf8               i
       #63 = Utf8               e
       #64 = Utf8               Ljava/lang/InterruptedException;
       #65 = Utf8               thread
       #66 = Utf8               Ljava/lang/Thread;
       #67 = Utf8               args
       #68 = Utf8               [Ljava/lang/String;
       #69 = Utf8               threads
       #70 = Utf8               [Ljava/lang/Thread;
       #71 = Utf8               StackMapTable
       #72 = Class              #70           // "[Ljava/lang/Thread;"
       #73 = Class              #68           // "[Ljava/lang/String;"
       #74 = Utf8               lambda$main$0
       #75 = Utf8               j
       #76 = Utf8               <clinit>
       #77 = Utf8               SourceFile
       #78 = Utf8               VolatileDemo.java
       #79 = Utf8               BootstrapMethods
       #80 = MethodHandle       6:#81         // REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #81 = Methodref          #82.#83       // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #82 = Class              #84           // java/lang/invoke/LambdaMetafactory
       #83 = NameAndType        #85:#86       // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #84 = Utf8               java/lang/invoke/LambdaMetafactory
       #85 = Utf8               metafactory
       #86 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #87 = MethodType         #6            //  ()V
       #88 = MethodHandle       6:#89         // REF_invokeStatic VolatileDemo.lambda$main$0:()V
       #89 = Methodref          #8.#90        // VolatileDemo.lambda$main$0:()V
       #90 = NameAndType        #74:#6        // lambda$main$0:()V
       #91 = MethodHandle       6:#92         // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
       #92 = Methodref          #93.#94       // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
       #93 = Class              #95           // java/lang/invoke/StringConcatFactory
       #94 = NameAndType        #41:#96       // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
       #95 = Utf8               java/lang/invoke/StringConcatFactory
       #96 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
       #97 = String             #98           // value的值:\u0001
       #98 = Utf8               value的值:\u0001
       #99 = Utf8               InnerClasses
      #100 = Class              #101          // java/lang/invoke/MethodHandles$Lookup
      #101 = Utf8               java/lang/invoke/MethodHandles$Lookup
      #102 = Class              #103          // java/lang/invoke/MethodHandles
      #103 = Utf8               java/lang/invoke/MethodHandles
      #104 = Utf8               Lookup
    {
      private static final int THREADS_COUNT;
        descriptor: I
        flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
        ConstantValue: int 10
    
      private static volatile int value;
        descriptor: I
        flags: (0x004a) ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE
    
      VolatileDemo();
        descriptor: ()V
        flags: (0x0000)
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 4: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LVolatileDemo;
    
      private static void increase();
        descriptor: ()V
        flags: (0x000a) ACC_PRIVATE, ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: getstatic     #7                  // Field value:I
             3: iconst_1
             4: iadd
             5: putstatic     #7                  // Field value:I
             8: return
          LineNumberTable:
            line 12: 0
            line 13: 8
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=5, locals=7, args_size=1
             0: bipush        10
             2: anewarray     #13                 // class java/lang/Thread
             5: astore_1
             6: iconst_0
             7: istore_2
             8: iload_2
             9: bipush        10
            11: if_icmpge     41
            14: aload_1
            15: iload_2
            16: new           #13                 // class java/lang/Thread
            19: dup
            20: invokedynamic #15,  0             // InvokeDynamic #0:run:()Ljava/lang/Runnable;
            25: invokespecial #19                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
            28: aastore
            29: aload_1
            30: iload_2
            31: aaload
            32: invokevirtual #22                 // Method java/lang/Thread.start:()V
            35: iinc          2, 1
            38: goto          8
            41: aload_1
            42: astore_2
            43: aload_2
            44: arraylength
            45: istore_3
            46: iconst_0
            47: istore        4
            49: iload         4
            51: iload_3
            52: if_icmpge     82
            55: aload_2
            56: iload         4
            58: aaload
            59: astore        5
            61: aload         5
            63: invokevirtual #25                 // Method java/lang/Thread.join:()V
            66: goto          76
            69: astore        6
            71: aload         6
            73: invokevirtual #30                 // Method java/lang/InterruptedException.printStackTrace:()V
            76: iinc          4, 1
            79: goto          49
            82: getstatic     #33                 // Field java/lang/System.out:Ljava/io/PrintStream;
            85: getstatic     #7                  // Field value:I
            88: invokedynamic #39,  0             // InvokeDynamic #1:makeConcatWithConstants:(I)Ljava/lang/String;
            93: invokevirtual #43                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            96: return
          Exception table:
             from    to  target type
                61    66    69   Class java/lang/InterruptedException
          LineNumberTable:
            line 17: 0
            line 18: 6
            line 19: 14
            line 24: 29
            line 18: 35
            line 27: 41
            line 29: 61
            line 32: 66
            line 30: 69
            line 31: 71
            line 27: 76
            line 34: 82
            line 35: 96
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                8      33     2     i   I
               71       5     6     e   Ljava/lang/InterruptedException;
               61      15     5 thread   Ljava/lang/Thread;
                0      97     0  args   [Ljava/lang/String;
                6      91     1 threads   [Ljava/lang/Thread;
          StackMapTable: number_of_entries = 6
            frame_type = 253 /* append */
              offset_delta = 8
              locals = [ class "[Ljava/lang/Thread;", int ]
            frame_type = 250 /* chop */
              offset_delta = 32
            frame_type = 254 /* append */
              offset_delta = 7
              locals = [ class "[Ljava/lang/Thread;", int, int ]
            frame_type = 255 /* full_frame */
              offset_delta = 19
              locals = [ class "[Ljava/lang/String;", class "[Ljava/lang/Thread;", class "[Ljava/lang/Thread;", int, int, class java/lang/Thread ]
              stack = [ class java/lang/InterruptedException ]
            frame_type = 250 /* chop */
              offset_delta = 6
            frame_type = 248 /* chop */
              offset_delta = 5
    
      private static void lambda$main$0();
        descriptor: ()V
        flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
        Code:
          stack=2, locals=1, args_size=0
             0: iconst_0
             1: istore_0
             2: iload_0
             3: sipush        1000
             6: if_icmpge     18
             9: invokestatic  #49                 // Method increase:()V
            12: iinc          0, 1
            15: goto          2
            18: return
          LineNumberTable:
            line 20: 0
            line 22: 9
            line 20: 12
            line 23: 18
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                2      16     0     j   I
          StackMapTable: number_of_entries = 2
            frame_type = 252 /* append */
              offset_delta = 2
              locals = [ int ]
            frame_type = 250 /* chop */
              offset_delta = 15
    
      static {};
        descriptor: ()V
        flags: (0x0008) ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: iconst_0
             1: putstatic     #7                  // Field value:I
             4: return
          LineNumberTable:
            line 8: 0
    }
    SourceFile: "VolatileDemo.java"
    BootstrapMethods:
      0: #80 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #87 ()V
          #88 REF_invokeStatic VolatileDemo.lambda$main$0:()V
          #87 ()V
      1: #91 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #97 value的值:\u0001
    InnerClasses:
      public static final #104= #100 of #102; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
    

    然后找到对应的increase方法的字节码字节码如下所示:

    private static void increase();
        descriptor: ()V
        flags: (0x000a) ACC_PRIVATE, ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: getstatic     #7                  // Field value:I
             3: iconst_1
             4: iadd
             5: putstatic     #7                  // Field value:I
             8: return
          LineNumberTable:
            line 12: 0
            line 13: 8
    

    可以看到value++由四条指令构成的,分别是getstaticiconst_1iaddputstaticgetstatic指令是获取静态字段value的值并且放入操作栈顶iconst_1指令是把常量1放入操作栈顶iadd指令是把当前操作栈顶中两个值相加并且把结果放入操作栈顶putstatic指令是把操作栈顶的结果赋值给静态变量value,关键字volatile可以保证执行getstatic指令后的值是正确的,如果在并发环境下,可能有其他线程在执行iconst_1指令或者iadd指令时,增加了value的值,导致操作栈顶的值就变成了过期的数据,在执行putstatic指令后可能把较小的value的值同步回主内存中,导致不能得到正确的结果

    从上面的例子可以得知,volatile变量只保证可见性,以下两条规则运算环境可以保证这些操作的原子性

    • 只有单条线程修改变量的值,运算结果不依赖变量当前的值,也就是说不依赖产生的中间结果。
    • 变量不需要与其他的状态变量共同参与不变约束。

    如果不符合以上两条规则的话,就需要通过加锁来保证这些操作的原子性,可以使用关键字synchronized或者java.util.concurrent中的原子类

    禁止指令重排序优化

    Java内存模型中的一个语义是线程内表现为串行的语义(Within-Thread As-If-Serial Semantics),它是指普通变量只能保证在该方法在执行过程中所有依赖赋值结果的地方都能得到正确的结果但是不保证变量的赋值操作的顺序和程序代码中的执行顺序是一致的。举个例子,代码如下所示:

    int i = 1;
    int j = 2;
    int k = i + j;
    

    上面这段代码大概执行了以下步骤:

    1. 将常量1赋值给i
    2. 将常量2赋值给j
    3. 取到i的值
    4. 取到j的值
    5. 将i的值和j的值相加后赋值给k

    在上面这五个步骤中,步骤1可能会和步骤2步骤4重排序,步骤2可能会和步骤1步骤3重排序,步骤3可能会和步骤2步骤4重排序,步骤4可能会和步骤1步骤3重排序,但是步骤1步骤3步骤5之间不能重排序步骤2步骤4步骤5之间不能重排序,因为它们之间存在依赖关系,一旦重排序线程表现为串行的语义无法得到保证

    再看个例子,使用双重检查锁定(DCL)实现单例模式,代码如下所示:

    /**
     * Created by TanJiaJun on 2020/8/23.
     */
    class Singleton {
    
        // 用关键字volatile修饰变量sInstance,禁止指令重排序优化
        private static volatile Singleton sInstance;
    
        // 私有构造方法
        private Singleton() {
            // 防止通过反射调用构造方法导致单例失效
            if (sInstance != null)
                throw new RuntimeException("Cannot construct a singleton more than once.");
        }
    
        // 获取单例的方法
        public static Singleton getInstance() {
            // 第一次判断sInstance是否为空,用于判断是否需要同步,提高性能和效率
            if (sInstance == null) {
                // 使用synchronized修饰代码块,取Singleton的Class对象作为锁对象
                synchronized (Singleton.class) {
                    // 第二次判断sInstance是否为空,用于判断是否已经创建实例
                    if (sInstance == null) {
                        // 创建Singleton对象
                        sInstance = new Singleton();
                    }
                }
            }
            // 返回sInstance
            return sInstance;
        }
    
        public static void main(String[] args) {
            Singleton.getInstance();
        }
    
    }
    

    然后使用HSDIS插件反汇编上面的代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithVolatile.log汇编代码如下所示:

    0x000000011b33f4c7:   mov    0x38(%rsp),%rax
      0x000000011b33f4cc:   movabs $0x61ff0ac48,%rdx            ;   {oop(a &apos;java/lang/Class&apos;{0x000000061ff0ac48} = &apos;Singleton&apos;)}
      0x000000011b33f4d6:   movsbl 0x30(%r15),%esi
      0x000000011b33f4db:   cmp    $0x0,%esi
      0x000000011b33f4de:   jne    0x000000011b33f6e9
      0x000000011b33f4e4:   mov    %rax,%r10
      0x000000011b33f4e7:   shr    $0x3,%r10
      0x000000011b33f4eb:   mov    %r10d,0x70(%rdx)
      0x000000011b33f4ef:   lock addl $0x0,-0x40(%rsp)
      0x000000011b33f4f5:   mov    %rdx,%rsi
      0x000000011b33f4f8:   xor    %rax,%rsi
      0x000000011b33f4fb:   shr    $0x15,%rsi
      0x000000011b33f4ff:   cmp    $0x0,%rsi
      0x000000011b33f503:   jne    0x000000011b33f708           ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}
                                                                ; - Singleton::getInstance@24 (line 25)
    

    然后把代码中的关键字volatile去掉,再生成汇编代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithNoVolatile.log汇编代码如下所示:

    0x0000000116f2a4c7:   mov    0x38(%rsp),%rax
      0x0000000116f2a4cc:   movabs $0x61ff0acb8,%rdx            ;   {oop(a &apos;java/lang/Class&apos;{0x000000061ff0acb8} = &apos;Singleton&apos;)}
      0x0000000116f2a4d6:   movsbl 0x30(%r15),%esi
      0x0000000116f2a4db:   cmp    $0x0,%esi
      0x0000000116f2a4de:   jne    0x0000000116f2a6e1
      0x0000000116f2a4e4:   mov    %rax,%r10
      0x0000000116f2a4e7:   shr    $0x3,%r10
      0x0000000116f2a4eb:   mov    %r10d,0x70(%rdx)
      0x0000000116f2a4ef:   mov    %rdx,%rsi
      0x0000000116f2a4f2:   xor    %rax,%rsi
      0x0000000116f2a4f5:   shr    $0x15,%rsi
      0x0000000116f2a4f9:   cmp    $0x0,%rsi
      0x0000000116f2a4fd:   jne    0x0000000116f2a700           ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}
                                                                ; - Singleton::getInstance@24 (line 25)
    

    通过对比可以发现,如果变量sInstance被关键字volatile修饰,会在赋值(mov %r10d,0x70(%rdx))后多执行一个lock addl 0x0,-0x40(%rsp)**指令,这个**指令**是一个**内存屏障(Memory Barrier)**,它可以使**内存屏障前的指令**和**内存屏障后的指令**不会因为系统优化而导致**乱序执行**,后面会详细讲解,**lock addl0x0,-0x40(%rsp)(%rsp是堆栈指针寄存器,通常会指向栈顶位置,堆栈的pop操作和push操作是通过改变%rsp的值来移动堆栈指针的位置来实现)是一个空操作,查询IA32手册可得知,使用这个空操作,而不是使用空操作指令nop是因为前缀lock不允许配合nop指令使用,其中前缀lock,查询IA32手册可得知,它的作用是使得本CPU的缓存写入内存相当于对缓存中的变量执行store操作和write操作这个写入动作可以让其他CPU或者别的内核无效化(Invalidata)其缓存可以让前面对被关键字volatile修饰的变量的修改对其他线程立即可见

    内存屏障

    内存屏障(Memory Barrier),也称为内存栅栏内存栅障屏障指令等,是一类同步屏障指令,它使得CPU或者编译器在对内存进行操作的时候,严格按照一定的顺序执行,大多数现代计算机为了提高性能而采用乱序执行,它就可以使内存屏障前的指令内存屏障后的指令不会因为系统优化而导致乱序执行

    内存屏障的语义是内存屏障前的所有写操作都要写入内存内存屏障后的所有读操作都可以获得同步屏障之前的读操作的结果

    内存屏障可以分为以下四种类型

    • LoadLoad屏障

      序列:①Load1②LoadLoad③Load2

      确保Load1载入的数据能够在被Load2后面的load指令载入数据前载入

    • StoreStore屏障

      序列:①Store1②StoreStore③Store2

      确保Store1存储的数据能够在Store2后面的store指令同步回主内存对其它处理器可见

    • LoadStore屏障

      序列:①Load1②LoadStore③Store2

      确保Load1载入的数据能够在Store2后面的store指令同步回主内存载入

    • StoreLoad屏障

      序列:①Store1②StoreLoad③Load2

      确保Store1存储的数据能够在Load2后面的load指令载入数据前对其它处理器可见。它是这四种内存屏障开销最大的,它也是一个万能屏障,具有其它三种内存屏障的功能。

    下图展示了这些内存屏障如何符合JSR-133排序规则:

    MemoryBarrierRule.png

    举个例子,代码如下所示:

    /**
     * Created by TanJiaJun on 2020/8/23.
     */
    class MemoryBarrierTest {
    
        private int a, b;
        private volatile int c, d;
    
        private void test() {
            int i, j;
            i = a; // load a
            j = b; // load b
            i = c; // load c
            // LoadLoad
            j = d; // load d
            // LoadStore
            a = i; // store a
            b = j; // store b
            // StoreStore
            c = i; // store c
            // StoreStore
            d = j; // store d
            // StoreLoad
            i = d; // load d
            // LoadLoad
            // LoadStore
            j = b; // load b
            a = i; // store a
        }
    
    }
    

    另外,为了保证关键字final特殊语义,会在下面的序列中加入内存屏障

    ①x.finalField = v;②StoreStore③sharedRef = x;

    总结

    总结下Java内存模型中对被关键字volatile修饰的变量进行read(读取)load(载入)use(使用)assign(赋值)store(存储)write(写入)操作定义的特殊规则

    • 假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A变量i执行的前一个操作是load操作的时候,线程A才能对变量i进行use操作;并且,只有线程A变量i执行的后一个操作是use操作的时候,线程A才能对变量i执行load操作,也就是说,线程A变量i执行use操作是和对其执行read操作load操作相关联的,它们都必须要连续一起出现

      这条规则要求在工作内存中,每次使用volatile变量都必须从主内存中刷新最新的值,用于保证能看见其他线程对volatile变量的修改后的值。

    • 假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A变量i执行的前一个操作是assign操作的时候,才能对其进行store操作;并且,只有线程A变量i执行后一个操作是store操作的时候,线程A才能对变量i进行assign操作,也就是说,线程A变量i执行assign操作是和对其执行store操作write操作相关联的,它们都必须要连续一起出现

      这条规则要求在工作内存中,每次修改volatile变量时都要立刻同步回主内存,用于保证其他线程能看见volatile变量修改后的值。

    • 假设有一个线程A,有两个被关键字volatile修饰的变量,分别为ij;假定动作A线程Avolatile变量i执行use操作或者assign操作,假定动作B是和动作A相关联的load操作或者store操作,假定动作C是和动作B相关联的read操作或者write操作;假定动作D线程Avolatile变量j执行use操作或者assign操作,假定动作E是和动作D相关联的load操作或者store操作,假定动作F是和动作E相关联的read操作或者write操作;如果动作A先于动作D,那么动作C先于动作F

      这条规则要求被关键字volatile修饰的变量不会被指令重排序优化,保证了代码的执行顺序和程序的顺序相同。

    题外话

    HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机的源码中,但是没有提供编译后的程序

    我讲解下如何用HSDIS插件查看类的汇编代码,步骤如下:

    1. 下载hsdis-amd64.dylib,链接如下:

      hsdis-amd64.dylib

    2. hsdis-amd64.dylib放在目录:/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/lib

    3. 配置IntelliJ IDEA运行参数,打开界面,如下图所示:

      ConfigureTheRunParametersInterface.png

      VM options填上运行参数,如下图所示:

      OperationParameters.png

      运行参数如下:

      -XX:+UnlockDiagnosticVMOptions
      -XX:+PrintAssembly
      -Xcomp
      -XX:CompileCommand=compileonly,*Sinlgeton.getInstance
      -XX:+LogCompilation
      -XX:LogFile=SingletonAssemblyCodeWithVolatile.log
      
      • -XX:+UnlockDiagnosticVMOptions解锁用于JVM诊断选项
      • -XX:+PrintAssembly:输出反汇编内容。
      • -Xcomp:让虚拟机编译模式执行代码,这样就可以不需要执行足够次数来预热就能触发JIT编译
      • -XX:CompileCommand=compileonly,Singleton.getInstance:让编译器不要内联getInstance方法并且只编译getInstance方法*。
      • -XX:+LogCompilation:允许将汇编代码记录到当前工作目录名为hotspot.log文件中,可以通过-XX:LogFile指定文件名字文件路径
      • -XX:LogFile=SingletonAssemblyCodeWithVolatile.log:指定汇编代码记录的文件名字文件路径,这里指定文件名字SingletonAssemblyCodeWithVolatile.log
    4. run,然后就会在项目目录下生成SingletonAssemblyCodeWithVolatile.log文件,查看这个文件就可以看到汇编代码了。

    我的GitHub:TanJiaJunBeyond

    Android通用框架:Android通用框架

    我的掘金:谭嘉俊

    我的简书:谭嘉俊

    我的CSDN:谭嘉俊

    相关文章

      网友评论

          本文标题:深入了解volatile关键字

          本文链接:https://www.haomeiwen.com/subject/hudgjktx.html