volatile底層原理詳解

今天我們聊聊volatile底層原理;

Java語言規範對於volatile定義如下:

Java編程語言允許線程訪問共享變量,爲了確保共享變量能夠被準確和一致性地更新,線程應該確保通過排它鎖單獨獲得這個變量。

首先我們從定義開始入手,官方定義比較拗口。通俗來說就是一個字段被volatile修飾,Java的內存模型確保所有的線程看到的這個變量值是一致的,但是它並不能保證多線程的原子操作。這就是所謂的線程可見性。我們要知道他是不能保證原子性的

內存模型相關概念

Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的修改何時對另外一個線程可見。JMM定義了線程與主內存的抽象關係:線程之間的變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory)保存着共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。

image

如果線程A與線程B通信:

  1. 線程A要先把本地內存A中更新過的共享變量刷寫到主內存中。

  2. 線程B到主內存中讀取線程A更新後的共享變量

計算機在運行程序時,每條指令都是在CPU中執行的,在執行過程中勢必會涉及到數據的讀寫。我們知道程序運行的數據是存儲在主存中,這時就會有一個問題,讀寫主存中的數據沒有CPU中執行指令的速度快,如果任何的交互都需要與主存打交道則會大大影響效率,所以就有了CPU高速緩存。CPU高速緩存爲某個CPU獨有,只與在該CPU運行的線程有關。

有了CPU高速緩存雖然解決了效率問題,但是它會帶來一個新的問題:數據一致性。在程序運行中,會將運行所需要的數據複製一份到CPU高速緩存中,在進行運算時CPU不再也主存打交道,而是直接從高速緩存中讀寫數據,只有當運行結束後纔會將數據刷新到主存中。

舉個例子:

i++;

當線程運行這行代碼時,首先會從主內存中讀取i,然後複製一份到CPU高速緩存中,接着CPU執行+1的操作,再將+1後的數據寫在緩存中,最後一步纔是刷新到主內存中。在單線程時沒有問題,多線程就有問題了。

如下:假如有兩個線程A、B都執行這個操作(i++),按照我們正常的邏輯思維主存中的i值應該=3,但事實是這樣麼?

分析如下:

兩個線程從主存中讀取i的值(1)到各自的高速緩存中,然後線程A執行+1操作並將結果寫入高速緩存中,最後寫入主存中,此時主存i==2,線程B做同樣的操作,主存中的i仍然=2。所以最終結果爲2並不是3。這種現象就是緩存一致性問題。

解決緩存一致性方案有兩種:

  1. 通過在總線加LOCK#鎖的方式;

  2. 通過緩存一致性協議。

但是方案1存在一個問題,它是採用一種獨佔的方式來實現的,即總線加LOCK#鎖的話,只能有一個CPU能夠運行,其他CPU都得阻塞,效率較爲低下。

第二種方案,緩存一致性協議(MESI協議)它確保每個緩存中使用的共享變量的副本是一致的。所以JMM就解決這個問題。

volatile實現原理

有volatile修飾的共享變量進行寫操作的時候會多出Lock前綴的指令,該指令在多核處理器下會引發兩件事情。

  1. 將當前處理器緩存行數據刷寫到系統主內存。

  2. 這個刷寫回主內存的操作會使其他CPU緩存的該共享變量內存地址的數據無效。

這樣就保證了多個處理器的緩存是一致的,對應的處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器緩存行設置無效狀態,當處理器對這個數據進行修改操作的時候會重新從主內存中把數據讀取到緩存裏。

使用場景

volatile經常用於兩個場景:狀態標記、double check

  1. 狀態標記
//線程1
boolean stop = false;
while(!stop){
   doSomething();
}

//線程2
stop = true;

這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會採用這種標記辦法。但是事實上,這段代碼會完全運行正確麼?即一定會將線程中斷麼?不一定,也許在大多數時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是隻要一旦發生這種情況就會造成死循環了)。

下面解釋一下這段代碼爲何有可能導致無法中斷線程。在前面已經解釋過,每個線程在運行過程中都有自己的工作內存,那麼線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內存當中。

那麼當線程2更改了stop變量的值之後,但是還沒來得及寫入主存當中,線程2轉去做其他事情了,那麼線程1由於不知道線程2對stop變量的更改,因此還會一直循環下去。

但是加上volatile就沒問題了。如下所示:

    volatile boolean flag = false;

    while(!flag){
       doSomething();
    }

    public void setFlag() {
       flag = true;
    }

    volatile boolean inited = false;
    //線程1:
    context = loadContext();  
    inited = true;            

    //線程2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);
  1. double check
public class Singleton{
   private volatile static Singleton instance = null;

   private Singleton() {

  }

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

客官覺得有用請點贊或收藏,關注公衆號JavaStorm,你將發現一個有趣的靈魂!
後面我們繼續分析JMM內存模型相關技術。
將自己的知識分享,以後會持續輸出,希望給讀者朋友們帶來幫助。若有幫助讀者朋友可以點贊或者關注。
JavaStorm.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章