1. 程式人生 > 實用技巧 >Java 併發程式設計之 JMM & volatile 詳解

Java 併發程式設計之 JMM & volatile 詳解

本文從計算機模型開始,以及CPU與記憶體、IO匯流排之間的互動關係到CPU快取一致性協議的邏輯進行了闡述,並對JMM的思想與作用進行了詳細的說明。針對volatile關鍵字從位元組碼以及彙編指令層面解釋了它是如何保證可見性與有序性的,最後對volatile進行了拓展,從實戰的角度更瞭解關鍵字的運用。

一、現代計算機理論模型與工作原理

1.1 馮諾依曼計算機模型

讓我們來一起回顧一下大學計算機基礎,現代計算機模型——馮諾依曼計算機模型,是一種將程式指令儲存器和資料儲存器合併在一起的計算機設計概念結構。依據馮·諾伊曼結構設計出的計算機稱做馮.諾依曼計算機,又稱儲存程式計算機。

計算機在執行指令時,會從儲存器中一條條指令取出,通過譯碼(控制器),從儲存器中取出資料,然後進行指定的運算和邏輯等操作,然後再按地址把運算結果返回記憶體中去。

接下來,再取出下一條指令,在控制器模組中按照規定操作。依此進行下去。直至遇到停止指令。

程式與資料一樣存貯,按程式編排的順序,一步一步地取出指令,自動地完成指令規定的操作是計算機最基本的工作模型。這一原理最初是由美籍匈牙利數學家馮.諾依曼於1945年提出來的,故稱為馮.諾依曼計算機模型。

  • 五大核心組成部分:

  1. 運算器:顧名思義,主要進行計算,算術運算、邏輯運算等都由它來完成。
  2. 儲存器:這裡儲存器只是記憶體,不包括記憶體,用於儲存資料、指令資訊。實際就是我們計算機中記憶體(RAM)
  3. 控制器:控制器是是所有裝置的排程中心,系統的正常執行都是有它來調配。CPU包含控制器和運算器。
  4. 輸入裝置:負責向計算機中輸入資料,如滑鼠、鍵盤等。
  5. 輸出裝置:負責輸出計算機指令執行後的資料,如顯示器、印表機等。

  • 現代計算機硬體結構:

圖中結構可以關注兩個重點:

I/O匯流排:所有的輸入輸出裝置都與I/O匯流排對接,儲存我們的記憶體條、USB、顯示卡等等,就好比一條公路,所有的車都在上面行駛,但是畢竟容量有限,IO頻繁或者資料較大時就會引起“堵車”

CPU:當CPU執行時最直接也最快的獲取儲存的是暫存器,然後會通過CPU快取從L1->L2->L3尋找,如果快取都沒有則通過I/O匯流排到記憶體中獲取,記憶體中獲取到之後會依次刷入L3->L2->L1->暫存器中。現代計算機上我們CPU一般都是 1.xG、2.xG的赫茲,而我們記憶體的速度只有每秒幾百M,所以為了為了不讓記憶體拖後腿也為了儘量減少I/O匯流排的互動,才有了CPU快取的存在,CPU型號的不同有的是兩級快取,有的是三級快取,執行速度對比:暫存器 > L1 > L2 > L3 > 記憶體條

1.2 CPU多級快取和記憶體

CPU快取即高速緩衝儲存器,是位於CPU與主記憶體之間容量很小但速度很高的儲存器。CPU直接從記憶體中存取資料後會儲存到快取中,當CPU再次使用時可以直接從快取中調取。如果有資料修改,也是先修改快取中的資料,然後經過一段時間之後才會重新寫回主記憶體中。

CPU快取最小單元是快取行(cache line),目前主流計算機的快取行大小為64Byte,CPU快取也會有LRU、Random等快取淘汰策略。CPU的三級快取為多個CPU共享的。

  • CPU讀取資料時的流程:

(1)先讀取暫存器的值,如果存在則直接讀取

(2)再讀取L1,如果存在則先把cache行鎖住,把資料讀取出來,然後解鎖

(3)如果L1沒有則讀取L2,如果存在則先將L2中的cache行加鎖,然後將資料拷貝到L1,再執行讀L1的過程,最後解鎖

(4)如果L2沒有則讀取L3,同上先加鎖,再往上層依次拷貝、加鎖,讀取到之後依次解鎖

(5)如果L3也沒有資料則通知記憶體控制器佔用匯流排頻寬,通知記憶體加鎖,發起記憶體讀請求,等待迴應,迴應資料儲存到L3(如果沒有就到L2),再從L3/2到L1,再從L1到CPU,之後解除匯流排鎖定。

  • 快取一致性問題:

在多處理器系統中,每個處理器都有自己的快取,於是也引入了新的問題:快取一致性。當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的快取資料不一致的情況。為了解決一致性的問題,需要各個處理器訪問快取時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI、MOSI等等。

1.3 MESI快取一致性協議

快取一致性協議中應用最廣泛的就是MESI協議。主要原理是 CPU 通過匯流排嗅探機制(監聽)可以感知資料的變化從而將自己的快取裡的資料失效,快取行中具體的幾種狀態如下:

以上圖為例,假設主記憶體中有一個變數x=1,CPU1和CPU2中都會讀寫,MESI的工作流程為:

(1)假設CPU1需要讀取x的值,此時CPU1從主記憶體中讀取到快取行後的狀態為E,代表只有當前快取中獨佔資料,並利用CPU嗅探機制監聽匯流排中是否有其他快取讀取x的操作。

(2)此時如果CPU2也需要讀取x的值到快取行,則在CPU2中快取行的狀態為S,表示多個快取中共享,同時CPU1由於嗅探到CPU2也快取了x所以狀態也變成了S。並且CPU1和CPU2會同時嗅探是否有另快取失效獲取獨佔快取的操作。

(3)當CPU1有寫入操作需要修改x的值時,CPU1中快取行的狀態變成了M。

(4)CPU2由於嗅探到了CPU1的修改操作,則會將CPU2中快取的狀態變成 I 無效狀態。

(5)此時CPU1中快取行的狀態重新變回獨佔E的狀態,CPU2要想讀取x的值的話需要重新從主記憶體中讀取。

二、JMM模型

2.1 Java 執行緒與系統核心的關係

Java執行緒在JDK1.2之前,是基於稱為“綠色執行緒”(Green Threads)的使用者執行緒實現的,而在JDK1.2中,執行緒模型替換為基於作業系統原生執行緒模型來實現。因此,在目前的JDK版本中,作業系統支援怎樣的執行緒模型,在很大程度上決定了Java虛擬機器的執行緒是怎樣對映的,這點在不同的平臺上沒有辦法達成一致,虛擬機器規範中也並未限定Java執行緒需要使用哪種執行緒模型來實現。

使用者執行緒:指不需要核心支援而在使用者程式中實現的執行緒,其不依賴於作業系統核心,應用程序利用執行緒庫提供建立、同步、排程和管理執行緒的函式來控制使用者執行緒。另外,使用者執行緒是由應用程序利用執行緒庫建立和管理,不依賴於作業系統核心。不需要使用者態/核心態切換,速度快。作業系統核心不知道多執行緒的存在,因此一個執行緒阻塞將使得整個程序(包括它的所有執行緒)阻塞。由於這裡的處理器時間片分配是以程序為基本單位,所以每個執行緒執行的時間相對減少。

核心執行緒:執行緒的所有管理操作都是由作業系統核心完成的。核心儲存執行緒的狀態和上下文資訊,當一個執行緒執行了引起阻塞的系統呼叫時,核心可以排程該程序的其他執行緒執行。在多處理器系統上,核心可以分派屬於同一程序的多個執行緒在多個處理器上執行,提高程序執行的並行度。由於需要核心完成執行緒的建立、排程和管理,所以和使用者級執行緒相比這些操作要慢得多,但是仍然比程序的建立和管理操作要快。

基於執行緒的區別,我們可以引出java記憶體模型的結構。

2.2 什麼是 JMM 模型

Java記憶體模型(Java Memory Model簡稱JMM)是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。

為了遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果,JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的:JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),用於儲存執行緒私有的資料,而Java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體拷貝的自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,不能直接操作主記憶體中的變數,工作記憶體中儲存著主記憶體中的變數副本拷貝。工作記憶體是每個執行緒的私有資料區域,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成。

主記憶體

主要儲存的是Java例項物件,所有執行緒建立的例項物件都存放在主記憶體中,不管該例項物件是成員變數還是方法中的本地變數(也稱區域性變數),當然也包括了共享的類資訊、常量、靜態變數。由於是共享資料區域,從某個程度上講應該包括了JVM中的堆和方法區。多條執行緒對同一個變數進行訪問可能會發生執行緒安全問題。

工作記憶體

主要儲存當前方法的所有本地變數資訊(工作記憶體中儲存著主記憶體中的變數副本拷貝),每個執行緒只能訪問自己的工作記憶體,即執行緒中的本地變數對其它執行緒是不可見的,就算是兩個執行緒執行的是同一段程式碼,它們也會各自在自己的工作記憶體中建立屬於當前執行緒的本地變數,當然也包括了位元組碼行號指示器、相關Native方法的資訊。所以則應該包括JVM中的程式計數器、虛擬機器棧以及本地方法棧。注意由於工作記憶體是每個執行緒的私有資料,執行緒間無法相互訪問工作記憶體,因此儲存在工作記憶體的資料不存線上程安全問題。

2.3 JMM 詳解

需要注意的是JMM只是一種抽象的概念,一組規範,並不實際存在。對於真正的計算機硬體來說,計算機記憶體只有暫存器、快取記憶體、主記憶體的概念。不管是工作記憶體的資料還是主記憶體的資料,對於計算機硬體來說都會儲存在計算機主記憶體中,當然也有可能儲存到CPU快取或者暫存器中,因此總體上來說,Java記憶體模型和計算機硬體記憶體架構是一個相互交叉的關係,是一種抽象概念劃分與真實物理硬體的交叉。

工作記憶體同步到主記憶體之間的實現細節,JMM定義了以下八種操作:

如果要把一個變數從主記憶體中複製到工作記憶體中,就需要按順序地執行read和load操作,如果把變數從工作記憶體中同步到主記憶體中,就需要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

  • 同步規則分析

(1)不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。

(2)一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或者assign)的變數。即就是對一個變數實施use和store操作之前,必須先自行assign和load操作。

(3)一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。lock和unlock必須成對出現。

(4)如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數之前需要重新執行load或assign操作初始化變數的值。

(5)如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。

(6)對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。

2.4JMM 如何解決多執行緒併發引起的問題

多執行緒併發下存在:原子性、可見性、有序性三種問題。

  • 原子性:

問題:原子性指的是一個操作是不可中斷的,即使是在多執行緒環境下,一個操作一旦開始就不會被其他執行緒影響。但是當執行緒執行的過程中,由於CPU上下文的切換,則執行緒內的多個操作並不能保證是保持原子執行。

解決:除了JVM自身提供的對基本資料型別讀寫操作的原子性外,可以通過 synchronized和Lock實現原子性。因為synchronized和Lock能夠保證任一時刻只有一個執行緒訪問該程式碼塊。

  • 可見性

問題:之前我們分析過,程式執行的過程中是分工作記憶體和主記憶體,工作記憶體將主記憶體中的變數拷貝到副本中快取,假如兩個執行緒同時拷貝一個變數,但是當其中一個執行緒修改該值,另一個執行緒是不可見的,這種工作記憶體和主記憶體之間的資料同步延遲就會造成可見性問題。另外由於指令重排也會造成可見性的問題。

解決:volatile關鍵字保證可見性。當一個共享變數被volatile修飾時,它會保證修改的值立即被其他的執行緒看到,即修改的值立即更新到主存中,當其他執行緒需要讀取時,它會去記憶體中讀取新值。synchronized和Lock也可以保證可見性,因為它們可以保證任一時刻只有一個執行緒能訪問共享資源,並在其釋放鎖之前將修改的變數重新整理到記憶體中。

有序性

問題:在單執行緒下我們認為程式是順序執行的,但是多執行緒環境下程式被編譯成機器碼的後可能會出現指令重排的現象,重排後的指令與原指令未必一致,則可能會造成程式結果與預期的不同。

解決:在Java裡面,可以通過volatile關鍵字來保證一定的有序性。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個執行緒執行同步程式碼,相當於是讓執行緒順序執行同步程式碼,自然就保證了有序性。

三、volatile關鍵字

3.1 volatile 的作用

volatile是 Java 虛擬機器提供的輕量級的同步機制。volatile關鍵字有如下兩個作用:

  • 保證被volatile修飾的共享變數對所有執行緒總數可見,也就是當一個執行緒修改了一個被volatile修飾共享變數的值,新值總是可以被其他執行緒立即得知

  • 禁止指令重排序優化

3.2 volatile 保證可見性

以下是一段多執行緒場景下存在可見性問題的程式。

public class VolatileTest extends Thread {
    private int index = 0;
    private boolean flag = false;
 
    @Override
    public void run() {
        while (!flag) {
            index++;
        }
    }
 
    public static void main(String[] args) throws Exception {
        VolatileTest volatileTest = new VolatileTest();
        volatileTest.start();
 
        Thread.sleep(1000);
 
        // 模擬多次寫入,並觸發JIT
        for (int i = 0; i < 10000000; i++) {
            volatileTest.flag = true;
        }
        System.out.println(volatileTest.index);
    }
}

執行可以發現,當 volatileTest.index 輸出列印之後程式仍然未停止,表示執行緒依然處於執行狀態,子執行緒讀取到的flag的值仍為false。

private volatile boolean flag = false;

嘗試給flag增加volatile關鍵字後程序可以正常結束, 則表示子執行緒讀取到的flag值為更新後的true。

那麼為什麼volatile可以保證可見性呢?

可以嘗試在JDK中下載hsdis-amd64.dll後使用引數-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 執行程式,可以看到程式被翻譯後的彙編指令,發現增加volatile關鍵字後給flag賦值時彙編指令多了一段 "lock addl $0x0,(%rsp)"

說明volatile保證了可見性正是這段lock指令起到的作用,查閱IA-32手冊,可以得知該指令的主要作用:

  • 鎖匯流排,其它CPU對記憶體的讀寫請求都會被阻塞,直到鎖釋放,不過實際後來的處理器都採用鎖快取替代鎖匯流排,因為鎖匯流排的開銷比較大,鎖匯流排期間其他CPU沒法訪問記憶體。
  • lock後的寫操作會回寫已修改的資料,同時讓其它CPU相關快取行失效,從而重新從主存中載入最新的資料。
  • 不是記憶體屏障卻能完成類似記憶體屏障的功能,阻止屏障兩遍的指令重排序。

3.3volatile 禁止指令重排

Java 語言規範規定JVM執行緒內部維持順序化語義。即只要程式的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以與程式碼順序不一致,此過程叫指令的重排序。指令重排序的意義是什麼?

JVM能根據處理器特性(CPU多級快取系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器效能。

以下是原始碼到最終執行的指令集的示例圖:

as-if-serial原則:不管怎麼重排序,單執行緒程式下編譯器和處理器不能對存在資料依賴關係的操作做重排序。但是,如果操作之間不存在資料依賴關係,這些操作就可能被編譯器和處理器重排序。

下面是一段經典的發生指令重排導致結果預期不符的例子:

public class VolatileTest {
 
    int a, b, x, y;
 
    public boolean test() throws InterruptedException {
        a = b = 0;
        x = y = 0;
        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
            b = 1;
            y = a;
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
 
        if (x == 0 && y == 0) {
            return true;
        } else {
            return false;
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; ; i++) {
            VolatileTest volatileTest = new VolatileTest();
            if (volatileTest.test()) {
                System.out.println(i);
                break;
            }
        }
    }
} 

按照我們正常的邏輯理解,在不出現指令重排的情況下,x、y永遠只會有下面三種情況,不會出現都為0,即迴圈永遠不會退出。

  1. x = 1、y = 1

  2. x = 1、y = 0

  3. x = 0、y = 1

但是當我們執行的時候會發現一段時間之後迴圈就會退出,即出現了x、y都為0的情況,則是因為出現了指令重排,時執行緒內的物件賦值順序發生了變化。

而這個問題給引數增加volatile關鍵字即可以解決,此處是因為JMM針對重排序問題限制了規則表。

為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。一個讀的操作為load,寫的操作為store。

對於編譯器來說,發現一個最優佈置來最小化插入屏障的總數幾乎不可能。為此,JMM採取保守策略。下面是基於保守策略的JMM記憶體屏障插入策略。

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。
  • 在每個volatile讀操作的後面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的後面插入一個LoadStore屏障。

以上圖為例,普通寫與volatile寫之間會插入一個StoreStore屏障,另外有一點需要注意的是,volatile寫後面可能有的volatile讀/寫操作重排序,因為編譯器常常無法準確判斷是否需要插入StoreLoad屏障。

則JMM採用了比較保守的策略:在每個volatile寫的後面插入一個StoreLoad屏障。

那麼存彙編指令的角度,CPU是怎麼識別到不同的記憶體屏障的呢:

1)sfence:實現Store Barrior 會將store buffer中快取的修改刷入L1 cache中,使得其他cpu核可以觀察到這些修改,而且之後的寫操作不會被排程到之前,即sfence之前的寫操作一定在sfence完成且全域性可見。

(2)lfence:實現Load Barrior 會將invalidate queue失效,強制讀取入L1 cache中,而且lfence之後的讀操作不會被排程到之前,即lfence之前的讀操作一定在lfence完成(並未規定全域性可見性)。

(3)mfence:實現Full Barrior 同時重新整理store buffer和invalidate queue,保證了mfence前後的讀寫操作的順序,同時要求mfence之後寫操作結果全域性可見之前,mfence之前寫操作結果全域性可見。

(4)lock:用來修飾當前指令操作的記憶體只能由當前CPU使用,若指令不操作記憶體仍然由用,因為這個修飾會讓指令操作本身原子化,而且自帶Full Barrior效果。

所以可以發現我們上述分析到的"lock addl"指令也是可以實現記憶體屏障效果的。

四、volatile 拓展

4.1 濫用 volatile 的危害

經過上述的總結我們可以知道volatile的實現是根據MESI快取一致性協議實現的,而這裡會用到CPU的嗅探機制,需要不斷對匯流排進行記憶體嗅探,大量的互動會導致匯流排頻寬達到峰值。因此濫用volatile可能會引起匯流排風暴,除了volatile之外大量的CAS操作也可能會引發這個問題。所以我們使用過程中要視情況而定,適當的場景下可以加鎖來保證執行緒安全。

4.2 如何不用 volatile 不加鎖禁止指令重排?

指令重排的示例中我們既然已經知道了插入記憶體屏障可以解決重排問題,那麼用什麼方式可以手動插入記憶體屏障呢?

JDK1.8之後可以在Unsafe魔術類中發現新增了插入屏障的方法。

/**
 * Ensures lack of reordering of loads before the fence
 * with loads or stores after the fence.
 * @since 1.8
 */
public native void loadFence();
 
/**
 * Ensures lack of reordering of stores before the fence
 * with loads or stores after the fence.
 * @since 1.8
 */
public native void storeFence();
 
/**
 * Ensures lack of reordering of loads or stores before the fence
 * with loads or stores after the fence.
 * @since 1.8
 */
public native void fullFence();

(1)loadFence()表示該方法之前的所有load操作在記憶體屏障之前完成。

(2)storeFence()表示該方法之前的所有store操作在記憶體屏障之前完成。

(3)fullFence()表示該方法之前的所有load、store操作在記憶體屏障之前完成。

可以看到這三個方法正式對應了CPU插入記憶體屏障的三個指令lfence、sfence、mfence。

因此我們如果想手動新增記憶體屏障的話,可以用Unsafe的這三個native方法完成,另外由於Unsafe必須由bootstrap類載入器載入,所以我們想使用的話需要用反射的方式拿到例項物件。

/**
 * 反射獲取到unsafe
 */
private Unsafe reflectGetUnsafe() throws NoSuchFieldException, IllegalAccessException {
    Field field = Unsafe.class.getDeclaredField("theUnsafe");
    field.setAccessible(true);
    return (Unsafe) field.get(null);
}
 
 
// 上述示例中手動插入記憶體屏障
Thread t1 = new Thread(() -> {
    a = 1;
    // 插入LoadStore()屏障
    reflectGetUnsafe().storeFence();
    x = b;
});
Thread t2 = new Thread(() -> {
    b = 1;
    // 插入LoadStore()屏障
    reflectGetUnsafe().storeFence();
    y = a;
});

  

4.3 單例模式的雙重檢查鎖為什麼需要用 volatile

以下是單例模式雙重檢查鎖的初始化方式:

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

因為synchronized雖然加了鎖,但是程式碼塊內的程式是無法保證指令重排的,其中instance = new Singleton(); 方法其實是拆分成多個指令,我們用javap -c 檢視位元組碼,可以發現這段物件初始化操作是分成了三步:

(1)new :建立物件例項,分配記憶體空間

(2)invokespecial :呼叫構造器方法,初始化物件

(3)aload_0 :存入區域性方法變量表

以上三步如果順序執行的話是沒問題的,但是如果2、3步發生指令重排,則極端併發情況下可能出現下面這種情況:

所以,為了保證單例物件順利的初始化完成,應該給物件加上volatile關鍵字禁止指令重排。

五、總結

隨著計算機和CPU的逐步升級,CPU快取幫我們大大提高了資料讀寫的效能,在高併發的場景下,CPU通過MESI快取一致性協議針對快取行的失效進行處理。基於JMM模型,將使用者態和核心態進行了劃分,通過java提供的關鍵字和方法可以幫助我們解決原子性、可見性、有序性的問題。其中volatile關鍵字的使用最為廣泛,通過新增記憶體屏障、lock彙編指令的方式保證了可見性和有序性,在我們開發高併發系統的過程中也要注意volatile關鍵字的使用,但是不能濫用,否則會導致匯流排風暴。

參考資料

  1. 書籍:《java併發程式設計實戰》

  2. IA-32手冊

  3. 雙重檢查鎖為什麼要使用volatile?

  4. java記憶體模型總結

  5. Java 8 Unsafe: xxxFence() instructions

作者:push