1. 程式人生 > >volatile變數記憶體可見性的原理分析—彙編指令分析

volatile變數記憶體可見性的原理分析—彙編指令分析

在java虛擬機器的記憶體模型中,有主記憶體和工作記憶體的概念,每個執行緒對應一個工作記憶體,並共享主記憶體的資料,下面看看操作普通變數和volatile變數有什麼不同:

1、對於普通變數:讀操作會優先讀取工作記憶體的資料,如果工作記憶體中不存在,則從主記憶體中拷貝一份資料到工作記憶體中;寫操作只會修改工作記憶體的副本資料,這種情況下,其它執行緒就無法讀取變數的最新值。

2、對於volatile變數,讀操作時JMM會把工作記憶體中對應的值設為無效,要求執行緒從主記憶體中讀取資料;寫操作時JMM會把工作記憶體中對應的資料重新整理到主記憶體中,這種情況下,其它執行緒就可以讀取變數的最新值。

volatile變數的記憶體可見性是基於記憶體屏障(Memory Barrier)實現的,什麼是記憶體屏障?記憶體屏障,又稱記憶體柵欄,是一個CPU指令。在程式執行時,為了提高執行效能,編譯器和處理器會對指令進行重排序,JMM為了保證在不同的編譯器和CPU上有相同的結果,通過插入特定型別的記憶體屏障來禁止特定型別的編譯器重排序和處理器重排序,插入一條記憶體屏障會告訴編譯器和CPU:不管什麼指令都不能和這條Memory Barrier指令重排序。

class Singleton {
    private volatile static Singleton instance;
    private int a;
    private int b;
    private int b;
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    a = 1;  // 1
                     b = 2;  // 2
                    instance = new Singleton();  // 3
                    c = a + b;  // 4
                }
            }
        }
        return instance;
    } 
}

通過觀察volatile變數和普通變數所生成的彙編程式碼可以發現,操作volatile變數會多出一個lock字首指令:

Java程式碼:

instance = new Singleton();

彙編程式碼:

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: **lock** addl $0x0,(%esp);

這個lock字首指令相當於上述的記憶體屏障,提供了以下保證:
1、將當前CPU快取行的資料寫回到主記憶體;
2、這個寫回記憶體的操作會導致在其它CPU裡快取了該記憶體地址的資料無效。

CPU為了提高處理效能,並不直接和記憶體進行通訊,而是將記憶體的資料讀取到內部快取(L1,L2)再進行操作,但操作完並不能確定何時寫回到記憶體,如果對volatile變數進行寫操作,當CPU執行到Lock字首指令時,會將這個變數所在快取行的資料寫回到記憶體,不過還是存在一個問題,就算記憶體的資料是最新的,其它CPU快取的還是舊值,所以為了保證各個CPU的快取一致性,每個CPU通過嗅探在總線上傳播的資料來檢查自己快取的資料有效性,當發現自己快取行對應的記憶體地址的資料被修改,就會將該快取行設定成無效狀態,當CPU讀取該變數時,發現所在的快取行被設定為無效,就會重新從記憶體中讀取資料到快取中。

總結:volatile變數的操作實際上是把鎖轉移到了硬體層面,手段是:通過彙編的lock指令。lock指令實現的過程:1,處理器環迅寫回記憶體,2其他處理器快取無效(快取一致性協議)。