深入了解volatile關(guān)鍵字

本文章已授權(quán)微信公眾號(hào)郭霖(guolin_blog)轉(zhuǎn)載。

本文章講解的內(nèi)容是深入了解volatile關(guān)鍵字,建議對(duì)著示例項(xiàng)目閱讀文章,示例項(xiàng)目鏈接如下:

VolatileDemo

查看匯編代碼hsdis-amd64.dylib文件鏈接如下:

hsdis-amd64.dylib

匯編代碼鏈接如下:

關(guān)鍵字volatileJava虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制,當(dāng)一個(gè)變量被關(guān)鍵字volatile修飾之后,它有如下兩個(gè)特性

  • 保證了這個(gè)變量對(duì)所有線程的可見(jiàn)性
  • 禁止指令重排序優(yōu)化

保證變量對(duì)所有線程的可見(jiàn)性

關(guān)鍵字volatile可以保證變量對(duì)所有線程的可見(jiàn)性,也就是當(dāng)一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即得到修改的值。普通變量是做不到這樣,普通變量的值需要通過(guò)主內(nèi)存線程之間傳遞,舉個(gè)例子:線程A修改一個(gè)普通變量的值,然后傳送給主內(nèi)存,另外一個(gè)線程B需要等到傳送完主內(nèi)存后才能夠從主內(nèi)存進(jìn)行讀取操作,這樣變量最新的值才會(huì)對(duì)線程B可見(jiàn)。

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

/**
 * 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() {
        // 對(duì)value變量進(jìn)行自增操作
        value++;
    }

    public static void main(String[] args) {
        // 創(chuàng)建10個(gè)線程
        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++)
                    // 每個(gè)線程對(duì)value變量進(jìn)行1000次自增操作
                    increase();
            });
            threads[i].start();
        }
        // 主線程等待子線程運(yùn)行結(jié)束
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("value的值:" + value);
    }

}

這段代碼的意思是發(fā)起10個(gè)線程,然后每個(gè)線程對(duì)value變量進(jìn)行1000次自增操作,如果這段代碼正確地并發(fā)操作,最后的結(jié)果value的值應(yīng)該是10000,但是實(shí)際上多次運(yùn)行后,value的值都是小于等于10000的值。

這段代碼中increase方法調(diào)用i++,也就是i = i + 1,它不是原子性操作Java內(nèi)存模型直接保證的原子性變量操作包括read、loadassign、use、storewrite,我們可以認(rèn)為基本數(shù)據(jù)類(lèi)型的讀寫(xiě)都具備原子性,有個(gè)例外就是longdouble非原子性協(xié)定,不過(guò)我們無(wú)須太過(guò)在意,雖然Java內(nèi)存模型允許虛擬機(jī)不把longdouble的變量的讀寫(xiě)實(shí)現(xiàn)為原子性操作,但是現(xiàn)在的商用虛擬機(jī)都幾乎把這些操作實(shí)現(xiàn)為原子性操作,原子性操作是指執(zhí)行一系列操作這些操作要么全部執(zhí)行,要么全部不執(zhí)行,不存在只執(zhí)行其中一部分的情況,舉個(gè)例子:i = 1就是個(gè)原子性操作,但是i = i + 1就不是原子性操作,因?yàn)檫@個(gè)操作是由多條字節(jié)碼指令構(gòu)成的,我用Javap反編譯上面的示例代碼,先找到生成的Class文件,路徑是/Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class,就是在VolatileDemo目錄下的out文件夾中,然后執(zhí)行javap -p -v VolatileDemo命令,生成如下字節(jié)碼

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

然后找到對(duì)應(yīng)的increase方法的字節(jié)碼字節(jié)碼如下所示:

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++由四條指令構(gòu)成的,分別是getstatic、iconst_1iaddputstatic,getstatic指令是獲取靜態(tài)字段value的值并且放入操作棧頂iconst_1指令是把常量1放入操作棧頂,iadd指令是把當(dāng)前操作棧頂中兩個(gè)值相加并且把結(jié)果放入操作棧頂,putstatic指令是把操作棧頂?shù)慕Y(jié)果賦值給靜態(tài)變量value,關(guān)鍵字volatile可以保證執(zhí)行getstatic指令后的值是正確的,如果在并發(fā)環(huán)境下,可能有其他線程在執(zhí)行iconst_1指令或者iadd指令時(shí),增加了value的值,導(dǎo)致操作棧頂?shù)闹稻妥兂闪诉^(guò)期的數(shù)據(jù),在執(zhí)行putstatic指令后可能把較小的value的值同步回主內(nèi)存中,導(dǎo)致不能得到正確的結(jié)果。

從上面的例子可以得知,volatile變量只保證可見(jiàn)性,以下兩條規(guī)則運(yùn)算環(huán)境可以保證這些操作的原子性

  • 只有單條線程修改變量的值,運(yùn)算結(jié)果不依賴變量當(dāng)前的值,也就是說(shuō)不依賴產(chǎn)生的中間結(jié)果。
  • 變量不需要與其他的狀態(tài)變量共同參與不變約束。

如果不符合以上兩條規(guī)則的話,就需要通過(guò)加鎖來(lái)保證這些操作的原子性,可以使用關(guān)鍵字synchronized或者java.util.concurrent中的原子類(lèi)。

禁止指令重排序優(yōu)化

Java內(nèi)存模型中的一個(gè)語(yǔ)義是線程內(nèi)表現(xiàn)為串行的語(yǔ)義(Within-Thread As-If-Serial Semantics),它是指普通變量只能保證在該方法在執(zhí)行過(guò)程中所有依賴賦值結(jié)果的地方都能得到正確的結(jié)果,但是不保證變量的賦值操作的順序和程序代碼中的執(zhí)行順序是一致的。舉個(gè)例子,代碼如下所示:

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

上面這段代碼大概執(zhí)行了以下步驟:

  1. 將常量1賦值給i
  2. 將常量2賦值給j
  3. 取到i的值
  4. 取到j(luò)的值
  5. 將i的值和j的值相加后賦值給k

在上面這五個(gè)步驟中,步驟1可能會(huì)和步驟2步驟4重排序,步驟2可能會(huì)和步驟1步驟3重排序,步驟3可能會(huì)和步驟2步驟4重排序,步驟4可能會(huì)和步驟1步驟3重排序,但是步驟1、步驟3步驟5之間不能重排序,步驟2步驟4步驟5之間不能重排序,因?yàn)?strong>它們之間存在依賴關(guān)系,一旦重排序線程表現(xiàn)為串行的語(yǔ)義無(wú)法得到保證。

再看個(gè)例子,使用雙重檢查鎖定(DCL)實(shí)現(xiàn)單例模式,代碼如下所示:

/**
 * Created by TanJiaJun on 2020/8/23.
 */
class Singleton {

    // 用關(guān)鍵字volatile修飾變量sInstance,禁止指令重排序優(yōu)化
    private static volatile Singleton sInstance;

    // 私有構(gòu)造方法
    private Singleton() {
        // 防止通過(guò)反射調(diào)用構(gòu)造方法導(dǎo)致單例失效
        if (sInstance != null)
            throw new RuntimeException("Cannot construct a singleton more than once.");
    }

    // 獲取單例的方法
    public static Singleton getInstance() {
        // 第一次判斷sInstance是否為空,用于判斷是否需要同步,提高性能和效率
        if (sInstance == null) {
            // 使用synchronized修飾代碼塊,取Singleton的Class對(duì)象作為鎖對(duì)象
            synchronized (Singleton.class) {
                // 第二次判斷sInstance是否為空,用于判斷是否已經(jīng)創(chuàng)建實(shí)例
                if (sInstance == null) {
                    // 創(chuàng)建Singleton對(duì)象
                    sInstance = new Singleton();
                }
            }
        }
        // 返回sInstance
        return sInstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }

}

然后使用HSDIS插件反匯編上面的代碼,我只截取了對(duì)變量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)

然后把代碼中的關(guān)鍵字volatile去掉,再生成匯編代碼,我只截取了對(duì)變量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)

通過(guò)對(duì)比可以發(fā)現(xiàn),如果變量sInstance被關(guān)鍵字volatile修飾,會(huì)在賦值(mov %r10d,0x70(%rdx))后多執(zhí)行一個(gè)lock addl 0x0,-0x40(%rsp)**指令,這個(gè)**指令**是一個(gè)**內(nèi)存屏障(Memory Barrier)**,它可以使**內(nèi)存屏障前的指令**和**內(nèi)存屏障后的指令**不會(huì)因?yàn)橄到y(tǒng)優(yōu)化而導(dǎo)致**亂序執(zhí)行**,后面會(huì)詳細(xì)講解,**lock addl0x0,-0x40(%rsp)(%rsp是堆棧指針寄存器,通常會(huì)指向棧頂位置,堆棧的pop操作和push操作是通過(guò)改變%rsp的值來(lái)移動(dòng)堆棧指針的位置來(lái)實(shí)現(xiàn))是一個(gè)空操作,查詢IA32手冊(cè)可得知,使用這個(gè)空操作,而不是使用空操作指令nop是因?yàn)榍熬Ylock不允許配合nop指令使用,其中前綴lock,查詢IA32手冊(cè)可得知,它的作用是使得本CPU的緩存寫(xiě)入內(nèi)存相當(dāng)于對(duì)緩存中的變量執(zhí)行store操作和write操作,這個(gè)寫(xiě)入動(dòng)作可以讓其他CPU或者別的內(nèi)核無(wú)效化(Invalidata)其緩存,可以讓前面對(duì)被關(guān)鍵字volatile修飾的變量的修改對(duì)其他線程立即可見(jiàn)。

內(nèi)存屏障

內(nèi)存屏障(Memory Barrier),也稱為內(nèi)存柵欄、內(nèi)存柵障屏障指令等,是一類(lèi)同步屏障指令,它使得CPU或者編譯器在對(duì)內(nèi)存進(jìn)行操作的時(shí)候,嚴(yán)格按照一定的順序執(zhí)行,大多數(shù)現(xiàn)代計(jì)算機(jī)為了提高性能而采用亂序執(zhí)行,它就可以使內(nèi)存屏障前的指令內(nèi)存屏障后的指令不會(huì)因?yàn)橄到y(tǒng)優(yōu)化而導(dǎo)致亂序執(zhí)行。

內(nèi)存屏障的語(yǔ)義是內(nèi)存屏障前的所有寫(xiě)操作都要寫(xiě)入內(nèi)存內(nèi)存屏障后的所有讀操作都可以獲得同步屏障之前的讀操作的結(jié)果。

內(nèi)存屏障可以分為以下四種類(lèi)型

  • LoadLoad屏障

    序列:①Load1②LoadLoad③Load2

    確保Load1載入的數(shù)據(jù)能夠在被Load2后面的load指令載入數(shù)據(jù)前載入

  • StoreStore屏障

    序列:①Store1②StoreStore③Store2

    確保Store1存儲(chǔ)的數(shù)據(jù)能夠在Store2后面的store指令同步回主內(nèi)存對(duì)其它處理器可見(jiàn)。

  • LoadStore屏障

    序列:①Load1②LoadStore③Store2

    確保Load1載入的數(shù)據(jù)能夠在Store2后面的store指令同步回主內(nèi)存載入。

  • StoreLoad屏障

    序列:①Store1②StoreLoad③Load2

    確保Store1存儲(chǔ)的數(shù)據(jù)能夠在Load2后面的load指令載入數(shù)據(jù)前對(duì)其它處理器可見(jiàn)。它是這四種內(nèi)存屏障開(kāi)銷(xiāo)最大的,它也是一個(gè)萬(wàn)能屏障,具有其它三種內(nèi)存屏障的功能。

下圖展示了這些內(nèi)存屏障如何符合JSR-133排序規(guī)則:

MemoryBarrierRule.png

舉個(gè)例子,代碼如下所示:

/**
 * 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
    }

}

另外,為了保證關(guān)鍵字final特殊語(yǔ)義,會(huì)在下面的序列中加入內(nèi)存屏障

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

總結(jié)

總結(jié)下Java內(nèi)存模型中對(duì)被關(guān)鍵字volatile修飾的變量進(jìn)行read(讀?。?/strong>、load(載入)use(使用)、assign(賦值)、store(存儲(chǔ))write(寫(xiě)入)操作定義的特殊規(guī)則

  • 假設(shè)有一個(gè)線程A,有一個(gè)被關(guān)鍵字volatile修飾的變量i;只有當(dāng)線程A對(duì)變量i執(zhí)行的前一個(gè)操作是load操作的時(shí)候,線程A才能對(duì)變量i進(jìn)行use操作;并且,只有線程A對(duì)變量i執(zhí)行的后一個(gè)操作是use操作的時(shí)候,線程A才能對(duì)變量i執(zhí)行load操作,也就是說(shuō),線程A對(duì)變量i執(zhí)行use操作是和對(duì)其執(zhí)行read操作load操作相關(guān)聯(lián)的,它們都必須要連續(xù)一起出現(xiàn)。

    這條規(guī)則要求在工作內(nèi)存中,每次使用volatile變量都必須從主內(nèi)存中刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)volatile變量的修改后的值。

  • 假設(shè)有一個(gè)線程A,有一個(gè)被關(guān)鍵字volatile修飾的變量i;只有當(dāng)線程A對(duì)變量i執(zhí)行的前一個(gè)操作是assign操作的時(shí)候,才能對(duì)其進(jìn)行store操作;并且,只有線程A對(duì)變量i執(zhí)行后一個(gè)操作是store操作的時(shí)候,線程A才能對(duì)變量i進(jìn)行assign操作,也就是說(shuō),線程A對(duì)變量i執(zhí)行assign操作是和對(duì)其執(zhí)行store操作write操作相關(guān)聯(lián)的,它們都必須要連續(xù)一起出現(xiàn)。

    這條規(guī)則要求在工作內(nèi)存中,每次修改volatile變量時(shí)都要立刻同步回主內(nèi)存,用于保證其他線程能看見(jiàn)volatile變量修改后的值。

  • 假設(shè)有一個(gè)線程A,有兩個(gè)被關(guān)鍵字volatile修飾的變量,分別為ij;假定動(dòng)作A線程A對(duì)volatile變量i執(zhí)行use操作或者assign操作,假定動(dòng)作B是和動(dòng)作A相關(guān)聯(lián)的load操作或者store操作,假定動(dòng)作C是和動(dòng)作B相關(guān)聯(lián)的read操作或者write操作;假定動(dòng)作D線程A對(duì)volatile變量j執(zhí)行use操作或者assign操作,假定動(dòng)作E是和動(dòng)作D相關(guān)聯(lián)的load操作或者store操作,假定動(dòng)作F是和動(dòng)作E相關(guān)聯(lián)的read操作或者write操作;如果動(dòng)作A先于動(dòng)作D,那么動(dòng)作C先于動(dòng)作F。

    這條規(guī)則要求被關(guān)鍵字volatile修飾的變量不會(huì)被指令重排序優(yōu)化,保證了代碼的執(zhí)行順序和程序的順序相同。

題外話

HSDIS是一個(gè)Sun官方推薦的HotSpot虛擬機(jī)JIT編譯代碼的反匯編插件,它包含在HotSpot虛擬機(jī)的源碼中,但是沒(méi)有提供編譯后的程序

我講解下如何用HSDIS插件查看類(lèi)的匯編代碼,步驟如下:

  1. 下載hsdis-amd64.dylib,鏈接如下:

    hsdis-amd64.dylib

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

  3. 配置IntelliJ IDEA運(yùn)行參數(shù),打開(kāi)界面,如下圖所示:

    ConfigureTheRunParametersInterface.png

    VM options填上運(yùn)行參數(shù),如下圖所示:

    OperationParameters.png

    運(yùn)行參數(shù)如下:

    -XX:+UnlockDiagnosticVMOptions
    -XX:+PrintAssembly
    -Xcomp
    -XX:CompileCommand=compileonly,*Sinlgeton.getInstance
    -XX:+LogCompilation
    -XX:LogFile=SingletonAssemblyCodeWithVolatile.log
    
    • -XX:+UnlockDiagnosticVMOptions解鎖用于JVM診斷選項(xiàng)。
    • -XX:+PrintAssembly:輸出反匯編內(nèi)容。
    • -Xcomp:讓虛擬機(jī)編譯模式執(zhí)行代碼,這樣就可以不需要執(zhí)行足夠次數(shù)來(lái)預(yù)熱就能觸發(fā)JIT編譯。
    • -XX:CompileCommand=compileonly,Singleton.getInstance:讓編譯器不要內(nèi)聯(lián)getInstance方法并且只編譯getInstance方法*。
    • -XX:+LogCompilation:允許將匯編代碼記錄到當(dāng)前工作目錄名為hotspot.log文件中,可以通過(guò)-XX:LogFile指定文件名字文件路徑。
    • -XX:LogFile=SingletonAssemblyCodeWithVolatile.log:指定匯編代碼記錄的文件名字文件路徑,這里指定文件名字SingletonAssemblyCodeWithVolatile.log。
  4. 點(diǎn)run,然后就會(huì)在項(xiàng)目目錄下生成SingletonAssemblyCodeWithVolatile.log文件,查看這個(gè)文件就可以看到匯編代碼了。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:譚嘉俊

我的簡(jiǎn)書(shū):譚嘉俊

我的CSDN:譚嘉俊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容