天天看點

深度解析volatile關鍵字,就是這麼簡單

點選上方 "程式員小樂"關注, 星标或置頂一起成長

背景回複“大禮包”有驚喜禮包!

深度解析volatile關鍵字,就是這麼簡單
關注訂閱号「程式員小樂」,收看更多精彩内容
           

每日英文

Sometimes, you don't get over things. You just learn to live with the pain.-有時候,我們并非走出了傷痛,不過是學會了帶着傷痛繼續生活。

每日掏心話

有時候你把什麼放下了,不是因為突然就舍得了,是因為期限到了,任性夠了,成熟多了,也就知道這一頁該翻過去了。

來自:譚嘉俊 | 責編:樂樂

連結:juejin.im/user/2400989124522446

程式員小樂(ID:study_tech)第 1036 次推文

往日回顧:快手公司廁所裝坑位計時器,網友:再也不能帶薪拉屎了!

   正文   

/   開始   /

本文章講解的内容是深入了解volatile關鍵字,建議對着示例項目閱讀文章,示例項目連結如下:

VolatileDemo

https://github.com/TanJiaJunBeyond/VolatileDemo

檢視彙編代碼的hsdis-amd64.dylib檔案連結如下:

hsdis-amd64.dylib

https://github.com/TanJiaJunBeyond/VolatileDemo/blob/master/hsdis-amd64.dylib

關鍵字volatile是Java虛拟機提供的最輕量級的同步機制,當一個變量被關鍵字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的值。

搜尋公衆号程式員小樂回複關鍵字“Java”,擷取Java面試題和答案。

這段代碼中increase方法調用i++,也就是i = i + 1,它不是原子性操作,Java記憶體模型直接保證的原子性變量操作包括read、load、assign、use、store和write,我們可以認為基本資料類型的讀寫都具備原子性,有個例外就是long和double的非原子性協定,不過我們無須太過在意,雖然Java記憶體模型允許虛拟機不把long和double的變量的讀寫實作為原子性操作。

但是現在的商用虛拟機都幾乎把這些操作實作為原子性操作,原子性操作是指執行一系列操作,這些操作要麼全部執行,要麼全部不執行,不存在隻執行其中一部分的情況,舉個例子:i = 1就是個原子性操作,但是i = i + 1就不是原子性操作,因為這個操作是由多條位元組碼指令構成的,我用Javap反編譯上面的示例代碼,先找到生成的Class檔案,路徑是

/Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class

就是在VolatileDemo目錄下的out檔案夾中,然後執行javap -p -v VolatileDemo指令,生成如下位元組碼:

(由于源碼過長,想詳細了解的可以到原文章閱讀)

然後找到對應的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++是由四條指令構成的,分别是getstatic、iconst_1、iadd和putstatic,getstatic指令是擷取靜态字段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::[email protected] (line 25)

           

然後把代碼中的關鍵字volatile去掉,再生成彙編代碼,我隻截取了對變量sInstance指派(第25行)的那部分彙編代碼,如果想要看全部的彙編代碼,可以在檢視SingletonAssemblyCodeWithNoVolatile.log,彙編代碼如下所示:

搜尋公衆号程式員小樂回複關鍵字“offer”,擷取算法面試題和答案。

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::[email protected] (line 25)

           

通過對比可以發現,如果變量sInstance被關鍵字volatile修飾,會在指派(mov    %r10d,0x70(%rdx))後多執行一個lock addl $0x0,-0x40(%rsp)指令,這個指令是一個記憶體屏障(Memory Barrier),它可以使記憶體屏障前的指令和記憶體屏障後的指令不會因為系統優化而導緻亂序執行,後面會詳細講解,lock addl $0x0,-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排序規則:

深度解析volatile關鍵字,就是這麼簡單

舉個例子,代碼如下所示:

/**
 * 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修飾的變量,分别為i和j;假定動作A是線程A對volatile變量i執行use操作或者assign操作,假定動作B是和動作A相關聯的load操作或者store操作,假定動作C是和動作B相關聯的read操作或者write操作;假定動作D是線程A對volatile變量j執行use操作或者assign操作,假定動作E是和動作D相關聯的load操作或者store操作,假定動作F是和動作E相關聯的read操作或者write操作;如果動作A先于動作D,那麼動作C先于動作F。

這條規則要求被關鍵字volatile修飾的變量不會被指令重排序優化,保證了代碼的執行順序和程式的順序相同。

深度解析volatile關鍵字,就是這麼簡單

歡迎在留言區留下你的觀點,一起讨論提高。如果今天的文章讓你有新的啟發,歡迎轉發分享給更多人。歡迎加入程式員小樂加群”或者“阿裡、騰訊、百度、華為、京東最新面試題彙集

delete後加 limit是個好習慣麼 !

Centos7搭建k8s環境教程,一次性成功,收藏了!

還在用if(obj!=null)做非空判斷?帶你快速上手Optional實戰性了解!

嘿,你在看嗎?

深度解析volatile關鍵字,就是這麼簡單