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協議,如此便可以輕量化的實現內存屏障;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM