volatile的理解

問:談談對volatile的理解?

當用volatile去申明一個變量時,就等於告訴虛擬機,這個變量極有可能會被某些程序或線程修改。爲了確保這個變量修改後,應用範圍內全部線程都能知道這個改動,虛擬機就要保證這個變量的可見性等特色。最簡單的一種方法就是加入volatile關鍵字。java

volatile是JVM提供的輕量級的同步機制。緩存

volatile有三大特性:安全

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排

要了解它的三大特性,要先了解JMM。多線程

JMM——Java內存模型

  • 因爲JVM運行程序的實體是線程,而每一個線程建立時JVM都會爲其建立一個工做內存,工做內存是每一個線程的私有數據區域,而Java內存模型中規定全部變量都存儲到主內存,主內存是共享內存區域,全部線程均可以訪問,但線程對變量的操做必須在工做內存中進行。
  • 首先要將變量從主內存拷貝到本身的工做內存空間,而後對變量進行操做,操做完成後再將變量寫回主內存,不能直接操做主內存中的變量,所以不一樣的線程間沒法訪問對方的工做內存,線程間的通訊(傳值)必須經過主內存來完成。

上面提到的概念 主內存 和 工做內存:併發

  • 主內存:就是計算機的內存。主要包括【本地方法區】和【堆】。
  • 工做內存:當同時有三個線程同時訪問student對象的age變量時,那麼每一個線程都會拷貝一份,到各自的工做內存。主要包括該線程私有的【棧】等。

如何保證可見性?

用代碼驗證volatile的可見性:性能

class MyData {
    // 定義int變量
    int number = 0;

    // 添加方法把變量 修改成 60
    public void addTo60() {
        this.number = 60;
    }
}

public class Test {
    public static void main(String[] args) {
        // 資源類
        MyData myData = new MyData();
        // 用lambda表達式建立線程
        new Thread(() -> {
            System.out.println("線程進來了");

            // 線程睡眠三秒,假設在進行運算
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 修改number的值
            myData.addTo60();
            // 輸出修改後的值
            System.out.println("線程更新了number的值爲" + myData.number);
        }).start();

        // main線程就一直在這裏等待循環,直到number的值不等於零
        while (myData.number == 0) {

        }
        
        //最後輸出這句話,看是否跳出了上一個循環
        System.out.println("main方法結束了");
    }
}

最後線程沒有中止,沒有輸出 main方法結束了 這句話,說明沒有用volatile修飾的變量,是沒有可見性的。優化

當咱們給變量 number 添加volatile關鍵字修飾時,發現能夠成功輸出結束語句。this

volatile 修飾的關鍵字,是爲了增長 主線程和線程之間的可見性,只要有一個線程修改了內存中的值,其它線程也能立刻感知,是具有JVM輕量級同步機制的。
  • volatile保證可見性用到了總線嗅探技術
  • 總線嗅探技術有哪些缺點:spa

    • 因爲Volatile的MESI緩存一致性協議,須要不斷的從主內存嗅探和CAS循環,無效的交互會致使總線帶寬達到峯值。所以不要大量使用volatile關鍵字,根據實際應用場景選擇。

Volatile不保證原子性

什麼是原子性?

不可分割,完整性。也就是說某個線程正在作某個具體業務時,中間不能夠被加塞或者被分割,須要具體完成,要麼同時成功,要麼同時失敗。線程

代碼證實volatile不保證原子性

class MyData {
    // 定義int變量
    volatile int number = 0;

    public void addPlusPlus() {
        number++;
    }
}

public class Test {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 建立20個線程,線程裏面進行1000次循環(20*1000=20000)
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {

                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }

            }).start();
        }

    /* 
        須要等待上面20個線程都執行完畢後,再用main線程取得最終的結果
        這裏判斷線程數是否大於2,爲何是2?由於默認有兩個線程的,一個main線程,一個gc線程
    */
        while (Thread.activeCount() > 2) {
            Thread.yield(); // yield表示不執行
        }

        System.out.println("線程運行完後,number的值爲:" + myData.number);
    }
}

線程執行完畢後,number輸出的值並無 20000,而是每次運行的結果都不一致,這說明了volatile修飾的變量不保證原子性。

爲何會出現數據丟失?

當 線程A 和 線程B 同時修改各自工做空間裏的內容,因爲可見性,須要將修改的值寫入主內存。這就致使多個線程出現同時寫入的狀況,線程A 寫的時候,線程B 也在寫入,致使其中的一個線程被掛起,其中一個線程覆蓋了另外一個線程的值,形成了數據的丟失。

i++是原子操做嗎?

i++不是原子操做,其執行要分爲三步:

  1. 讀內存到寄存器
  2. 在寄存器內自增
  3. 寫回內存

舉個例子:如今有A、B兩個線程,i 初始爲 2。A線程完成第二步的加一操做後,被切換到B線程,B線程中執行完這三步後,再切換回來。此時A寄存器中的 i=3 寫回內存,最後 i 的值不是正常的4。

若是解決原子性的問題?

  • 在方法上加上synchronized
public synchronized void addPlusPlus() {
    number ++;
}

引入synchronized關鍵字後,保證了該方法每次只可以一個線程進行訪問和操做,保證最後輸出的結果。

  • AtomicInteger

咱們還可使用JUC下面的原子包裝類i++可使用AtomicInteger來代替

//建立一個原子Integer包裝類,默認爲0
AtomicInteger number = new AtomicInteger();

public void addAtomic(){
    number.getAndIncrement();    //至關於number++
}

Volatile禁止指令重排

計算機在執行程序時,爲了提升性能,編譯器和處理器經常會對指令重排,通常分爲如下三種:

源代碼 -> 編譯器優化的重排 -> 指令並行的重排 -> 內存系統的重排 -> 最終執行指令。

多線程環境中線程交替執行,因爲編譯器優化重排的存在,兩個線程中使用的變量可否保證一致性是沒法確認的,結果沒法預測。

舉一個指令重排的例子

public void mySort() {
    int x = 11;    
    int y = 12;
    x = x + 5;
    y = x * x;
}

按照正常單線程環境,執行順序是1234。

可是在多線程環境中,可能出現如下的順序:213四、1324。

可是指令排序也是有限制的,例如3不能出如今1面前,由於3須要依賴步驟1的聲明,存在數據依賴。

Volatile針對指令重排作了啥?

Volatile實現禁止指令重排優化,從而避免了多線程環境下程序出現亂序執行的現象。

首先了解一個概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的做用有兩個:

  • 保證特定操做的順序
  • 保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)

在Volatile的寫和讀的時候,加入屏障,防止出現指令重排,線程安全得到保障。

Volatile的應用

  • 單線程下的單例模式代碼(懶漢,適用於單線程)
public class SingletonDemo {
    //用靜態變量保存這個惟一的實例
    private static SingletonDemo instance = null;
    
    //構造器私有化
    private SingletonDemo() {
        
    }
    
    //提供一個靜態方法,來獲取實例對象
    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }
}

單線程下建立出來的都是同一個對象。可是在多線程的環境下,咱們經過SingletonDemo.getInstance() 獲取到的對象,並非同一個。

  • 一、方法上引入synchronized
public synchronized static SingletonDemo getInstance() {
    if (instance == null) {
        instance = new SingletonDemo();
    }
    return instance;
}

可是synchronizaed屬於重量級的同步機制,它只容許一個線程同時訪問獲取實例的方法,可是所以減低了併發性,所以採用的比較少。

  • 二、引入DCL雙端檢鎖機制

就是在 進來、出去 的時候,進行檢測。

public static SingletonDemo getInstance() {
    if (instance == null) {
        synchronized (SingletonDemo.class) {
            if (instance == null) {
                instance = new SingletonDemo();
            }
        }
    }
    return instance;
}

可是DCL機制不必定是線程安全的,緣由是由於有指令重排的存在,咱們加入Volatile能夠禁止指令重排。

private static volatile SingletonDemo instance = null;

由於instance的獲取能夠分爲三步進行完成:

  1. 分配對象內存空間
  2. 初始化對象
  3. 設置instance指向剛剛分配的內存地址,此時instance != null

由於步驟二、3不存在數據依賴,便可能出現第三步先於第二步執行;此時由於已經給即將建立的instance分配了內存空間,因此instance!=null,但對象的初始化還未完成,形成了線程的安全問題。

-

題外話:單例模式雙重校驗的目的

去掉第一個判斷爲空:即懶漢式(線程安全),這會致使全部線程在調用getInstance()方法的時候,直接排隊等待同步鎖,而後等到排到本身的時候進入同步處理時,纔去校驗實例是否爲空,這樣子作會耗費不少時間(即線程安全,但效率低下)。

去掉第二個判斷爲空:即懶漢式(線程不安全),這會出現 線程A先執行了getInstance()方法,同時線程B在由於同步鎖而在外面等待,等到A線程已經建立出來一個實例出來而且執行完同步處理後,B線程將得到鎖並進入同步代碼,若是這時B線程不去判斷是否已經有一個實例了,而後直接再new一個。這時就會有兩個實例對象,即破壞了設計的初衷。(即線程不安全,效率高)

雙重校驗的目的:除了第一次實例化須要進行加鎖同步,以後的線程只要進行第一層的if判斷不爲空便可直接返回,而不用每一次獲取單例都加鎖同步,所以相比前面兩種懶漢式,雙重檢驗鎖更佳。(雙重校驗鎖結合了 兩種懶漢式 的優勢)

相關文章
相關標籤/搜索