Java volatile 原理解析

用 volatile 修飾的變量能夠保證其對(duì)所有線程的可見性,要理解這一點(diǎn),我們首先需要了解 Java 的內(nèi)存模型。

1. Java 內(nèi)存模型

Java 內(nèi)存模型分為主內(nèi)存和工作內(nèi)存。

主內(nèi)存是對(duì)所有線程所共享的,此外每個(gè)線程有自己的工作內(nèi)存,工作內(nèi)存不共享。

線程在工作時(shí),從主內(nèi)存中拷貝所需變量到自己的工作內(nèi)存中。

線程對(duì)變量的所有操作,都必須在工作內(nèi)存中進(jìn)行,不能直接操作主存中的變量,也不能直接訪問其他線程的工作內(nèi)存。

線程間變量值的傳遞需要通過(guò)主內(nèi)存進(jìn)行,何時(shí)將工作內(nèi)存中的變量同步到主內(nèi)存,由 JVM 控制。

Java 內(nèi)存模型

基于此種內(nèi)存模型,在多線程中會(huì)產(chǎn)生臟讀,即讀到非最新的數(shù)據(jù)。

譬如,有1個(gè)共享變量:

int i = 0;

線程A和線程B同時(shí)執(zhí)行以下操作:

i++;

我們期望的結(jié)果為 2,但實(shí)際結(jié)果可能為 1 也可能是 2。

我們分析一下線程的執(zhí)行過(guò)程:首先從主內(nèi)存中拷貝 變量i 到自己的工作內(nèi)存,對(duì)工作內(nèi)存中的 變量i 副本進(jìn)行 +1 操作,將 i 的最新值寫入到主內(nèi)存中。

當(dāng) 2 個(gè)線程同時(shí)執(zhí)行上述代碼時(shí),可能存在以下一種情況:線程A從主存中讀取了 變量i 到工作內(nèi)存中,并對(duì) i 進(jìn)行 +1 操作。在線程A將最新值 i=1 寫入到主存前,此時(shí)線程B從主存中讀取了 變量i,此時(shí) i 仍為 0。線程A、B分別將操作后的 變量i 的值同步到主存,最終在主存中 i = 1。

在上述例子中,線程A和B的工作內(nèi)存是相互隔離、不可訪問的,即不可見。
那么 volatile 能實(shí)現(xiàn)的可見性是什么呢,是能讓1個(gè)線程的工作內(nèi)存變成共享的嗎?并非如此,我們看下 Java中可見性的定義。

2. Java 中的可見性

可見性是指當(dāng)一個(gè)線程修改共享變量,其他線程下次讀取到的將是該共享變量的最新值。

上文說(shuō)到線程的工作內(nèi)存對(duì)其他線程是隔離的,那么如何保證其他線程讀到的是最新值呢?

事實(shí)上,當(dāng)一個(gè)共享變量用 volatile 關(guān)鍵字修飾時(shí),它會(huì)保證修改的值會(huì)被立即更新到主存中,同時(shí)其他線程的工作內(nèi)存中該共享變量的緩存將失效,當(dāng)線程下次讀取該變量時(shí),將強(qiáng)制主存中讀取最新值。

接下來(lái)從硬件的角度,簡(jiǎn)要說(shuō)下 volatile 的實(shí)現(xiàn)原理。

3. volatile 實(shí)現(xiàn)原理

Java 虛擬機(jī)規(guī)范定義了 Java 內(nèi)存模型來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存差異。

但是為了實(shí)現(xiàn)更好的執(zhí)行性能,Java 內(nèi)存模型沒有限制執(zhí)行引擎使用CPU的特定緩存器或緩存來(lái)和主內(nèi)存交互。

為方便理解,我們將 Java 內(nèi)存模型中的主內(nèi)存類比為 RAM(系統(tǒng)內(nèi)存),工作內(nèi)存類比為 CPU的高速緩存。

實(shí)際上,工作內(nèi)存并非獨(dú)立存在的一段內(nèi)存空間,它是對(duì)CPU的寄存器、高速緩存及其他硬件的抽象描述。

我們看下 CPU 和 系統(tǒng)內(nèi)存間的交互:

CPU 和主內(nèi)存交互示意圖

對(duì)于多核心處理器,每個(gè)處理器都有自己的高速緩存,用于緩存計(jì)算中間結(jié)果。當(dāng)不同核心上執(zhí)行的運(yùn)算任務(wù)涉及到同一塊內(nèi)存區(qū)域時(shí),就有可能出現(xiàn)緩存不一致的問題。

我們看看 Java 虛擬機(jī)是如何解決這個(gè)問題的,當(dāng)對(duì)用 volatile 修飾的變量進(jìn)行了寫操作時(shí),JVM 會(huì)向 CPU 發(fā)送一條 Lock 前綴的指令,該指令將做以下2件事情:

  • 將 CPU 高速緩存中的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存;
  • 如果其他 CPU 核心緩存了該數(shù)據(jù),將其置為失效。

其中 操作2 是通過(guò)緩存一致性協(xié)議實(shí)現(xiàn)的: 每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù),檢查自己緩存中的數(shù)據(jù)是否過(guò)期,當(dāng)處理器發(fā)現(xiàn)高速緩存中的數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址被修改,會(huì)將該緩存數(shù)據(jù)置為失效,當(dāng)處理器下次訪問該內(nèi)存地址數(shù)據(jù)時(shí),將強(qiáng)制重新從系統(tǒng)內(nèi)存中讀取。

4. volatile 的使用說(shuō)明

我們已經(jīng)解釋了 volatile 的實(shí)現(xiàn)原理和作用,那么它能避免文章開頭提到的臟讀問題嗎?答案是并不能,原因是 volatile 不能實(shí)現(xiàn)原子性操作。

原子性操作是不可再拆分的操作,要么執(zhí)行,要么不執(zhí)行。
原子操作不會(huì)被線程調(diào)度機(jī)制打斷,不需要 synchronized。

i++ 雖然僅包含一行語(yǔ)句,但實(shí)際上它進(jìn)行了 3 項(xiàng)操作:

  1. 從內(nèi)存中讀取 i 的至;
  2. 對(duì) i 進(jìn)行 +1 操作
  3. 將 i 的新值寫入到內(nèi)存中。

即便 volatile 能夠保證 線程A 進(jìn)行了 i+1 操作后,i 的新值將被立即更新到主存。但在 i 寫入到主存前,可能線程B已經(jīng)讀取了 i 值,此時(shí) i 仍為 0。在線程A 將 i 的新值寫到主存后,線程B 的工作內(nèi)存中 i 的緩存將失效,但此時(shí)線程B已無(wú)需再讀取 i 值。所以兩次 +1 操作后最終 i=1。

執(zhí)行以下代碼能證明上文這一點(diǎn):

public class VolatileDemo {
    private volatile int i;

    public void inc() {
        i++;
    }

    public int getI() {
        return i;
    }
    
    public static void main(String[] args) {
        final VolatileDemo test = new VolatileDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.inc();
                }
            }).start();
        }

        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(test.getI());
    }
}

執(zhí)行多次,發(fā)現(xiàn)控制臺(tái)打印的 i 值總是小于預(yù)期結(jié)果 10 * 1000 = 10000.

若要達(dá)到預(yù)期效果,則必須讓 i++ 變?yōu)樵硬僮?,這就需要通過(guò) synchronized 實(shí)現(xiàn),將 inc() 函數(shù)改為:

public synchronized void inc() {
    i++;
}

則每次執(zhí)行結(jié)果都為 10000.

由于 volatile 無(wú)法保證操作的原子性,在多線程場(chǎng)景下使用 volatile 需要保證以下2點(diǎn):

  1. 對(duì)變量的寫操作不依賴于當(dāng)前值;
  2. 該變量不會(huì)與其他變量被一起納入到不變性條件中(譬如下界 <= 上界)。

下面舉幾個(gè)應(yīng)用場(chǎng)景:

  • 用作狀態(tài)標(biāo)記
  volatile boolean shutdownFlag;  
  
  public void shutdown() {   
      shutdownFlag = true;   
  }  
  
  public void doWork() {   
     while (!shutdownFlag) {   
          doSomething(); 
      }  
  }  

在 shutdown() 方法中,shutdownFlag = true 的賦值操作,與 shutdownFlag的當(dāng)前值無(wú)關(guān)。而上文中的 i++ 操作,i 的新值依賴于當(dāng)前值。

  • 雙重檢查
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;
    }
}

在1個(gè)線程執(zhí)行完語(yǔ)句 instance = new Singleton(); 后,等待在同步鎖外的其他線程在判斷 if (instance == null) 時(shí),會(huì)重新從主存中讀取 instance 變量,從而發(fā)現(xiàn)其已構(gòu)造完畢,方法實(shí)現(xiàn)了單例模式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容