天天看點

面試官問我Volatile的原理?那吹呗!誰怕誰!

在多線程并發程式設計中,synchronized和volatile都扮演着及其重要的角色;可以這麼說,Volatile是輕量級的synchronized!volatile他在多處理器開發中保證了共享變量的可見性!也能保證在多線程并發情況中指令重排序的情況!

什麼是可見性?

電腦處理器為了提高運作速度,是以不會直接與記憶體進行互動!而是先會将資料讀取到内部緩存!之後在進行操作,操作完之後滿足一定條件之後,才會将内部緩存的資料寫進記憶體!是以,多線程共享變量,可能會存在髒讀的現象,也就是,明明已經将資料更改!但是卻會出現因為各個處理器内部緩存沒有更新,所導緻的髒讀現象!volatile的存在就是為了解決這個問題!使用了volatile聲明的變量會将這個資料在緩存行的資料寫入到記憶體中!同時各個處理器通過嗅探在總線上傳播的資料來檢查自己的緩存是不是過期了!進而保證資料對于各個線程和處理器的可見性!

那麼,volatile是如何保證可見性的呢?

我們先看一段代碼!

public class TestVolatile{
    public static volatile int value;
    public static void main(String[] args) {
           int a = 10;
           value = 9;
           value += a;
    }
}
           

我們知道,java代碼在編譯後,會被編譯成位元組碼!最終位元組碼被類加載器加載到JVM裡面!JVM執行位元組碼最終也需要轉換為彙編指令在CPU上運作!那麼我們就将這段代碼編譯為彙編語言,看一下volatile修飾的變量,到底做了什麼操作!保證了可見性!

0x00007f96b93132ed: lock addl $0x0,(%rsp)     ;*putstatic value
                                                ; - TestVolatile::[email protected] (line 6)
           

重點關注

lock addl $0x0,(%rsp)

通過查驗 IA-32架構軟體開發人員手冊 發現,帶有lock字首的的指令在多核處理器會發生兩件事:

  1. 将目前處理器緩存行的資料寫回到系統記憶體裡面去
  2. 這個寫回記憶體的操作會使其他CPU緩存行的資料無效

是以說在這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀取到緩存行中!

volatile的定義:在java語言規範第三版中對volatile的定義如下,Java程式設計語言,允許線程通路共享變量,為了確定共享變量能夠被準确和一緻的更新,線程應該使用排他鎖來單獨擷取這個變量!

Lock字首指令導緻在執行指令期間,聲言處理器的LOCK#信号。在多處理器環境中,LOCK#信号確定在聲言該信号期間,處理器可以獨占任何共享記憶體!

為什麼 處理器可以獨占任何共享記憶體呢?

因為它會鎖住總線,導緻其他CUP不能通路總線,不能通路總線就意味着不能通路系統記憶體!

總線鎖定把CPU和記憶體的通信給鎖住了,使得在鎖定期間,其他處理器不能操作其他記憶體位址的資料,進而開銷較大,是以後來的CPU都提供了緩存一緻性機制,Intel的奔騰486之後就提供了這種優化。

緩存一緻性:緩存一緻性機制就整體來說,是當某塊CPU對緩存中的資料進行操作了之後,就通知其他CPU放棄儲存在它們内部的緩存,或者從主記憶體中重新讀取,用MESI闡述原理如下:

MESI協定:是以緩存行(緩存的基本資料機關,在Intel的CPU上一般是64位元組)的幾個狀态來命名的(全名是Modified、Exclusive、 Share or Invalid)。該協定要求在每個緩存行上維護兩個狀态位,使得每個資料機關可能處于M、E、S和I這四種狀态之一,各種狀态含義如下:

  • M:被修改的。處于這一狀态的資料,隻在本CPU中有緩存資料,而其他CPU中沒有。同時其狀态相對于記憶體中的值來說,是已經被修改的,且沒有更新到記憶體中。
  • E:獨占的。處于這一狀态的資料,隻有在本CPU中有緩存,且其資料沒有修改,即與記憶體中一緻。
  • S:共享的。處于這一狀态的資料在多個CPU中都有緩存,且與記憶體一緻。
  • I:無效的。本CPU中的這份緩存已經無效。

​ 一個處于M狀态的緩存行,必須時刻監聽所有試圖讀取該緩存行對應的主存位址的操作,如果監聽到,則必須在此操作執行前把其緩存行中的資料寫回CPU。

​ 一個處于S狀态的緩存行,必須時刻監聽使該緩存行無效或者獨享該緩存行的請求,如果監聽到,則必須把其緩存行狀态設定為I。

​ 一個處于E狀态的緩存行,必須時刻監聽其他試圖讀取該緩存行對應的主存位址的操作,如果監聽到,則必須把其緩存行狀态設定為S。

​ 當CPU需要讀取資料時,如果其緩存行的狀态是I的,則需要從記憶體中讀取,并把自己狀态變成S,如果不是I,則可以直接讀取緩存中的值,但在此之前,必須要等待其他CPU的監聽結果,如其他CPU也有該資料的緩存且狀态是M,則需要等待其把緩存更新到記憶體之後,再讀取。

​ 當CPU需要寫資料時,隻有在其緩存行是M或者E的時候才能執行,否則需要發出特殊的RFO指令(Read Or Ownership,這是一種總線事務),通知其他CPU置緩存無效(I),這種情況下性能開銷是相對較大的。在寫入完成後,修改其緩存狀态為M。

​ 是以如果一個變量在某段時間隻被一個線程頻繁地修改,則使用其内部緩存就完全可以辦到,不涉及到總線事務,如果緩存一會被這個CPU獨占、一會被那個CPU 獨占,這時才會不斷産生RFO指令影響到并發性能。

其實JDK7的并發包中,著名的并發程式設計大師,Doug lea 新增了一個隊列集合 Linked-TransferQueue 他用了一種特殊的方式優化了volatile,是一種追加位元組的方式!我們以後可能會出一個詳解的,想要探究他,就一定要探究到處理器的硬體配置!我們有時間再說!

關于可見性的一個小案例

public class NoVisibility {
    private static boolean ready;
    private static class ReaderThread extends Thread{
        public void run(){
            while (!ready) {
                System.out.println(3);
            }
            System.out.println("-------------我是咋執行的??-----------------");
        }
    }
    public static void main(String args[]) throws Exception{
        new ReaderThread().start();
        ready=true;
    }
}
           

對于上面的一個代碼,正常情況下,他應該一直輸出3,但是如果發生髒讀的情況!也就是緩存行的資料沒有更新,那麼有可能執行這個代碼:

什麼是指令重排序

在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排序。
面試官問我Volatile的原理?那吹呗!誰怕誰!
  1. 編譯器優化的重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-LevelParallelism,ILP)來将多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

volatile是如何防止指令重排序的?

從彙編語言中可以看到在對volatile變量指派後會加一條

lock addl $0x0,(%rsp)

指令;lock指令具有記憶體屏障的作用,lock前後的指令不會重排序;

**記憶體屏障:**CPU術語定義是一組處理器指令,用于實作對記憶體操作的順序限制!

在hotspot源碼中記憶體屏障也是使用這樣的指令實作的,沒使用mfence指令,hotspot中解釋說mfence有時候開銷會很大。

記憶體屏障的功能,java解釋器遇到volatile變量,會在volatile變量指派之後,加一個lock addl $0x0,(%rsp)具有記憶體屏障功能的指令,防止記憶體重排序。

可能咋這麼說不太好了解,我們舉個例子來說明一下:

package com.zanzan.test;

public class TestVolatile {
    int a = 0;
    boolean flag = false;

    public void testA(){
        //語句1
        a = 1;
        //語句2
        flag = true;
    }

    public void testB(){
        if (flag){
            a = a + 5;
            System.out.println(a);
        }
    }

    public static void main(String[] args) {
        TestVolatile testVolatile = new TestVolatile();
        new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testA();
            }
        },"testVolatileA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testVolatile.testB();
            }
        },"testVolatileB").start();
    }

}
           

正常情況下結果是:6

但是發生指令重排後,語句2先執行,執行後線程時間片切換;線程2執行testB(),此時a = 0 那麼此時結果為 :5

這就是指令重排序!

我們使用

volatile

就可以解決:如何解決呢?

volatile boolean  flag = false;
volatile int a = 0;
           

如果你覺的還可以!歡迎關注作者哦! 公衆号:【JAVA程式狗】

面試官問我Volatile的原理?那吹呗!誰怕誰!