回覆列表
  • 1 # 程式猿W
    JMM 基礎-計算機原理

    Java 記憶體模型即Java Memory Model,簡稱JMM,JMM定義了Java 虛擬機器(JVM)在計算機(RAM) 中的工作方式。 JVM 是整個計算機虛擬模型,所以JMM是隸屬於JVM的,

    在計算機系統中,暫存器是L0級快取,接著依次是L1,L2,L3(接下來是記憶體,本地磁碟,遠端儲存).越往上的快取儲存空間越小,速度越快,成本也越高。越往下的儲存空間越大,速度更慢,成本也越低。

    從上至下,每一層都都可以是看作是更下一層的快取,即:L0暫存器是L1一級快取的快取,

    L1是L2的快取,一次類推;每一層的資料都是來至於它的下一層。

    在現在CPU上,一般來說L0,L1,L2,L3都繼承在CPU內部,而L1還分為一級資料快取和一級指令快取,分別用於存放資料和執行資料的指令解碼,每個核心擁有獨立的運算處理單元、控制器、暫存器、L1快取、L2 快取,然後一個CPU的多個核心共享最後一層CPU快取L3。

    CPU 的快取一致性解決方案

    分為以下兩種方案

    匯流排鎖(每次鎖匯流排,是悲觀鎖)

    快取鎖(只鎖快取的資料)

    MESI協議如下:

    M(modify):I(invalid)E(Exclusive)S(Share)JMM記憶體模型的八種同步操作

    1、read(讀取),從主記憶體讀取資料

    2、load(載入):將主記憶體讀取到的資料寫入到工作記憶體

    3、use(使用): 從工作記憶體讀取資料來計算

    4、assign(賦值):將計算好的值重新賦值到工作記憶體中

    5、store(儲存):將工作記憶體資料寫入主記憶體

    6、write(寫入):將store過去的變數值賦值給主記憶體中的變數

    7、lock(鎖定):將主記憶體變數加鎖,標識為執行緒 獨佔狀態

    8、unlock(解鎖):將主記憶體變數解鎖,解鎖後其他執行緒可以鎖定該變數

    Java 記憶體模型帶來的問題

    1、可見性問題

    左邊CPU中執行的執行緒從主記憶體中複製物件obj到它的CPU快取,把物件obj的count變數改為2,但這個變更對執行在右邊的CPU中的執行緒是不可見,因為這個更改還沒有flush到主記憶體中。

    在多執行緒環境下,如果某個執行緒首次讀取共享變數,則首先到主記憶體中獲取該變數,然後存入到工作記憶體中,以後只需要在工作記憶體中讀取該變數即可,同樣如果對該變數執行了修改的操作,則先將新值寫入工作記憶體中,然後再重新整理至於記憶體中,但是什麼時候最新的值會被重新整理到主記憶體中是不太確定的,一般來說是很快的,但是具體時間未知,,要解決共享物件可見性問題,我們可以使用volatile關鍵字或者加鎖。

    2、競爭問題

    執行緒A 和 執行緒B 共享一個物件obj, 假設執行緒A從主存讀取obj.count變數到自己的快取中,同時,執行緒B也讀取了obj.count變數到它的CPU快取,並且這兩個執行緒都對obj.count做了加1操作,此時,obj.count加1操作被執行了兩次,不過都在不同的CPU快取中。

    如果則兩個加1操作是序列執行的,那麼obj.count變數便會在原始值上加2,最終主記憶體中obj.count的值會為3,然後圖中兩個加1操作是並行的,不管是執行緒A還是執行緒B先flush計算結果到主存,最終主存中的obj.count只會增加1次變成2,儘管一共有兩次加1操作,要解決上面的問題我們可以使用synchronized 程式碼塊。

    3、重排序

    除了共享記憶體和工作記憶體帶來的問題,還存在重排序的問題,在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。

    重排序分3中型別:

    (1) 編譯器最佳化的重排序。

    (2) 指令級並行的重排序

    (3)記憶體系統的重排序

    ① 資料依賴性

    資料依賴性: 如果兩個操作訪問同一變數,且這兩個操作中有一個為寫,此時這兩個操作之間就存在資料依賴性。

    依賴性分為以下三種:

    上圖很明顯,A和C存在資料依賴,B和C也存在資料依賴,而A和B之間不存在資料依賴,如果重排序了A和C或者B和C的執行順序,程式的執行結果就會被改變。

    4、as - if -serial

    意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as - if -serial 語義。

    A和C之間存在資料依賴,同時B和C之間也存在資料依賴關係,因此在最終執行的指令序列中,C不能被重排序A和B的前面(C排到A和B的前面,程式的結果將會被改變)。但A和B之間沒有資料依賴關係,編譯器和處理器可以重排序A和B之間的執行順序。

    as - if -serial 語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器可以讓我們感覺到: 單執行緒程式看起來是按程式的順序來執行的。as-if-srial語義使單執行緒程式無需擔心重排序干擾他們,也無需擔心記憶體可見性的問題。

    5、記憶體屏障

    Java 編譯器在生成指令序列的適當位置會插入記憶體屏障來禁止特定型別的處理去重排序,從而讓程式按我們預想的流程去執行。

    ① 保證特定操作的執行順序

    ② 影響某些資料(或者是某條指令的執行結果)的記憶體可見性

    編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試最佳化效能。插入一條Memory Barrier 會告訴編譯器和CPU ,不管什麼指令都不能和這條Memory Barrier 指令重排序。

    Memory Barrier 所做的另外一件事是強制刷出各種CPU cache, 如一個Write-Barrier(寫入屏障)將刷出所在的Barrier 之前寫入cache的資料,因此,任何CPU上的執行緒都能讀取到這些資料的最新版本。

    JMM把記憶體屏障指令分為4類:

    StoreLoad Barriers 是一個"全能型"的屏障,它同時具有其他3個屏障的效果,

    volatile 關鍵字介紹

    1、保證可見性

    對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫。

    我們先看下面程式碼:

    initFlag 沒有用volatile關鍵字修飾;

    上面結果為:

    說明一個執行緒改變initFlag狀態,另外一個執行緒看不見;

    如果加上volatile關鍵字呢?

    結果如下:

    我們透過彙編看下程式碼的最終底層實現:

    volatile寫的記憶體語義如下:

    當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體。

    當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。

    比如:

    如果我們將flag變數以volatile關鍵字修飾,那麼實際上:執行緒A在寫flag變數後,本地記憶體A中被執行緒A更新過的兩個共享變數的值都被重新整理到主記憶體中。

    在讀flag變數後,本地記憶體B包含的值已經被置為無效。此時,執行緒B必須從主記憶體中讀取共享變數。執行緒B的讀取操作將導致本地記憶體B與主記憶體中的共享變數的值變成一致。

    如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀執行緒B讀一個volatile變數後,寫執行緒A在寫這個volatile變數之前所有可見的共享變數的值都將立即變得對讀執行緒B可見。

    2、原子性

    volatile 不保證變數的原子性;

    執行結果如下:

    因為count ++;

    包含 三個操作:

    (1) 讀取變數count

    (2) 將count變數的值加1

    (3) 將計算後的值再賦給變數count

    從JMM記憶體分析:

    下面從位元組碼分析為什麼i++這種的用volatile修改不能保證原子性?

    javap : 位元組碼檢視

    其實i++這種操作主要可以分為3步:(彙編)

    讀取volatile變數值到local增加變數的值把local的值寫回,讓其它的執行緒可見

    Load到store到記憶體屏障,一共4步,其中最後一步jvm讓這個最新的變數的值在所有執行緒可見,也就是最後一步讓所有的CPU核心都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。

    3、有序性

    (1) volatile重排序規則表

    ① 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。

    ② 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。

    (2) volatile的記憶體屏障

    ① volatile寫

    storestore屏障:對於這樣的語句store1; storestore; store2,在store2及後續寫入操作執行前,保證store1的寫入操作對其它處理器可見。(也就是說如果出現storestore屏障,那麼store1指令一定會在store2之前執行,CPU不會store1與store2進行重排序)

    storeload屏障:對於這樣的語句store1; storeload; load2,在load2及後續所有讀取操作執行前,保證store1的寫入對所有處理器可見。(也就是說如果出現storeload屏障,那麼store1指令一定會在load2之前執行,CPU不會對store1與load2進行重排序

    ② volatile讀

    在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個loadstore屏障。

    loadload屏障:對於這樣的語句load1; loadload; load2,在load2及後續讀取操作要讀取的資料被訪問前,保證load1要讀取的資料被讀取完畢。(也就是說,如果出現loadload屏障,那麼load1指令一定會在load2之前執行,CPU不會對load1與load2進行重排序)

    loadstore屏障:對於這樣的語句load1; loadstore; store2,在store2及後續寫入操作被刷出前,保證load1要讀取的資料被讀取完畢。(也就是說,如果出現loadstore屏障,那麼load1指令一定會在store2之前執行,CPU不會對load1與store2進行重排序)

    volatile的實現原理

    volatile的實現原理

    ❶ 透過對OpenJDK中的unsafe.cpp原始碼的分析,會發現被volatile關鍵字修飾的變數會存在一個“lock:”的字首。

    ❷ Lock字首,Lock不是一種記憶體屏障,但是它能完成類似記憶體屏障的功能。Lock會對CPU匯流排和快取記憶體加鎖,可以理解為CPU指令級的一種鎖。

    ❸ 同時該指令會將當前處理器快取行的資料直接寫會到系統記憶體中,且這個寫回記憶體的操作會使在其他CPU裡快取了該地址的資料無效。

    ❹ 具體的執行上,它先對匯流排和快取加鎖,然後執行後面的指令,最後釋放鎖後會把快取記憶體中的髒資料全部重新整理回主記憶體。在Lock鎖住匯流排的時候,其他CPU的讀寫請求都會被阻塞,直到鎖釋放。

  • 2 # JavaKeeper

    從幾道相關面試題說起吧,volatile 常被這麼問:

    談談你對 volatile 的理解?

    你知道 volatile 底層的實現機制嗎?

    volatile 變數和 atomic 變數有什麼不同?

    volatile 的使用場景,你能舉兩個例子嗎?

    我們知道Java 記憶體模型——JMM, JMM是圍繞著併發過程中如何處理可見性、原子性和有序性這 3 個 特徵建立起來的,而 volatile 可以保證其中的兩個特性,下面具體探討下這個面試必問的關鍵字。

    1. 概念

    volatile 是 Java 中的關鍵字,是一個變數修飾符,用來修飾會被不同執行緒訪問和修改的變數。

    2. Java 記憶體模型 3 個特性

    2.1 可見性

    可見性是一種複雜的屬性,因為可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的執行緒能適時地看到其他執行緒寫入的值,有時甚至是根本不可能的事情。為了確保多個執行緒之間對記憶體寫入操作的可見性,必須使用同步機制。

    可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果。另一個執行緒馬上就能看到。

    在 Java 中 volatile、synchronized 和 final 都可以實現可見性。

    2.2 原子性

    原子性指的是某個執行緒正在執行某個操作時,中間不可以被加塞或分割,要麼整體成功,要麼整體失敗。比如 a=0;(a非long和double型別) 這個操作是不可分割的,那麼我們說這個操作是原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。Java的 concurrent 包下提供了一些原子類,AtomicInteger、AtomicLong、AtomicReference等。

    在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。

    2.3 有序性

    Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證執行緒之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個物件鎖的兩個同步塊只能序列執行。

    3. volatile 是 Java 虛擬機器提供的輕量級的同步機制

    保證可見性

    不保證原子性

    禁止指令重排(保證有序性)

    3.1 空說無憑,程式碼驗證

    3.1.1 可見性驗證

    class MyData {

    int number = 0;

    public void add() {

    this.number = number + 1;

    }

    }

    // 啟動兩個執行緒,一個work執行緒,一個main執行緒,work執行緒修改number值後,檢視main執行緒的number

    private static void testVolatile() {

    MyData myData = new MyData();

    new Thread(() -> {

    System.out.println(Thread.currentThread().getName()+"\t come in");

    try {

    TimeUnit.SECONDS.sleep(2);

    myData.add();

    System.out.println(Thread.currentThread().getName()+"\t update number value :"+myData.number);

    } catch (InterruptedException e) {

    e.printStackTrace();

    }

    }, "workThread").start();

    //第2個執行緒,main執行緒

    while (myData.number == 0){

    //main執行緒還在找0

    }

    System.out.println(Thread.currentThread().getName()+"\t mission is over");

    System.out.println(Thread.currentThread().getName()+"\t mission is over,main get number is:"+myData.number);

    }

    }

    執行 方法,輸出如下,會發現在 main 執行緒死迴圈,說明 main 執行緒的值一直是 0

    workThread execute

    workThread update number value :1

    修改 ,,在 number 前加關鍵字 volatile,重新執行,main 執行緒獲取結果為 1

    workThread execute

    workThread update number value :1

    main execute over,main get number is:1

    3.1.2 不保證原子性驗證

    class MyData {

    volatile int number = 0;

    public void add() {

    this.number = number + 1;

    }

    }

    private static void testAtomic() throws InterruptedException {

    MyData myData = new MyData();

    for (int i = 0; i < 10; i++) {

    new Thread(() ->{

    for (int j = 0; j < 1000; j++) {

    myData.addPlusPlus();

    }

    },"addPlusThread:"+ i).start();

    }

    //等待上邊20個執行緒結束後(預計5秒肯定結束了),在main執行緒中獲取最後的number

    TimeUnit.SECONDS.sleep(5);

    while (Thread.activeCount() > 2){

    Thread.yield();

    }

    System.out.println("final value:"+myData.number);

    }

    執行 發現最後的輸出值,並不一定是期望的值 10000,往往是比 10000 小的數值。

    final value:9856

    為什麼會這樣呢,因為 在轉化為位元組碼指令的時候是4條指令

    獲取原始值

    將值入棧

    進行加 1 操作

    把 後的操作寫回主記憶體

    這樣在執行時候就會存在多執行緒競爭問題,可能會出現了丟失寫值的情況。

    如何解決原子性問題呢?

    加 或者直接使用 原子類。

    3.1.3 禁止指令重排驗證

    計算機在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排,一般分為以下 3 種

    處理器在進行重排序時必須要考慮指令之間的資料依賴性,我們叫做 語義

    單執行緒環境裡確保程式最終執行結果和程式碼順序執行的結果一致;但是多執行緒環境中執行緒交替執行,由於編譯器最佳化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的,結果無法預測。

    我們往往用下面的程式碼驗證 volatile 禁止指令重排,如果多執行緒環境下,`最後的輸出結果不一定是我們想象到的 2,這時就要把兩個變數都設定為 volatile。

    public class ReSortSeqDemo {

    int a = 0;

    boolean flag = false;

    public void mehtod1(){

    a = 1;

    flag = true;

    }

    public void method2(){

    if(flag){

    a = a +1;

    System.out.println("reorder value: "+a);

    }

    }

    }

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

    還有一個我們最常見的多執行緒環境中 版本的單例模式中,就是使用了 volatile 禁止指令重排的特性。

    public class Singleton {

    private static volatile Singleton instance;

    private Singleton(){}

    // DCL

    public static Singleton getInstance(){

    if(instance ==null){ //第一次檢查

    synchronized (Singleton.class){

    if(instance == null){ //第二次檢查

    instance = new Singleton();

    }

    }

    }

    return instance;

    }

    }

    因為有指令重排序的存在,雙端檢索機制也不一定是執行緒安全的。

    why ?

    Because: 初始化物件的過程其實並不是一個原子的操作,它會分為三部分執行,

    給 instance 分配記憶體呼叫 instance 的建構函式來初始化物件將 instance 物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)

    步驟 2 和 3 不存在資料依賴關係,如果虛擬機器存在指令重排序最佳化,則步驟 2和 3 的順序是無法確定的。如果A執行緒率先進入同步程式碼塊並先執行了 3 而沒有執行 2,此時因為 instance 已經非 null。這時候執行緒 B 在第一次檢查的時候,會發現 instance 已經是 非null 了,就將其返回使用,但是此時 instance 實際上還未初始化,自然就會出錯。所以我們要限制例項物件的指令重排,用 volatile 修飾(JDK 5 之前使用了 volatile 的雙檢鎖是有問題的)。

    4. 原理

    volatile 可以保證執行緒可見性且提供了一定的有序性,但是無法保證原子性。在 JVM 底層是基於記憶體屏障實現的。

    當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體複製變數到 CPU 快取中。如果計算機有多個CPU,每個執行緒可能在不同的 CPU 上被處理,這意味著每個執行緒可以複製到不同的 CPU cache 中而宣告變數是 volatile 的,JVM 保證了每次讀變數都從記憶體中讀,跳過 CPU cache 這一步,所以就不會有可見性問題對 volatile 變數進行寫操作時,會在寫操作後加一條 store 屏障指令,將工作記憶體中的共享變數重新整理回主記憶體;對 volatile 變數進行讀操作時,會在寫操作後加一條 load 屏障指令,從主記憶體中讀取共享變數;

    透過 hsdis 工具獲取 JIT 編譯器生成的彙編指令來看看對 volatile 進行寫操作CPU會做什麼事情,還是用上邊的單例模式,可以看到

    PS:具體的彙編指令對我這個 Javaer 太南了,但是 JVM 位元組碼我們可以認識, 的含義是給一個靜態變數設定值,那這裡的 ,而且是第 17 行程式碼,更加確定是給 instance 賦值了。果然像各種資料裡說的,找到了 據說還得翻閱。這裡可以看下這兩篇 https://www.jianshu.com/p/6ab7c3db13c3 、 https://www.cnblogs.com/xrq730/p/7048693.html )

    有 volatile 修飾的共享變數進行寫操作時會多出第二行彙編程式碼,該句程式碼的意思是對原值加零,其中相加指令addl前有 lock 修飾。透過查IA-32架構軟體開發者手冊可知,lock字首的指令在多核處理器下會引發兩件事情:

    將當前處理器快取行的資料寫回到系統記憶體

    這個寫回記憶體的操作會引起在其他CPU裡快取了該記憶體地址的資料無效

    正是 lock 實現了 volatile 的「防止指令重排」「記憶體可見」的特性

    5. 使用場景

    您只能在有限的一些情形下使用 volatile 變數替代鎖。要使 volatile 變數提供理想的執行緒安全,必須同時滿足下面兩個條件:

    對變數的寫操作不依賴於當前值

    該變數沒有包含在具有其他變數的不變式中

    其實就是在需要保證原子性的場景,不要使用 volatile。

    6. volatile 效能

    volatile 的讀效能消耗與普通變數幾乎相同,但是寫操作稍慢,因為它需要在原生代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。

    引用《正確使用 volaitle 變數》一文中的話:

    很難做出準確、全面的評價,例如 “X 總是比 Y 快”,尤其是對 JVM 內在的操作而言。(例如,某些情況下 JVM 也許能夠完全刪除鎖機制,這使得我們難以抽象地比較 和 的開銷。)就是說,在目前大多數的處理器架構上,volatile 讀操作開銷非常低 —— 幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現記憶體界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低。

    volatile 操作不會像鎖一樣造成阻塞,因此,在能夠安全使用 volatile 的情況下,volatile 可以提供一些優於鎖的可伸縮特性。如果讀操作的次數要遠遠超過寫操作,與鎖相比,volatile 變數通常能夠減少同步的效能開銷。

    參考

    《深入理解Java虛擬機器》

    http://tutorials.jenkov.com/java-concurrency/java-memory-model.html https://juejin.im/post/5dbfa0aa51882538ce1a4ebc

    《正確使用 Volatile 變數》

    https://www.ibm.com/developerworks/cn/java/j-jtp06197.htm

    l

  • 3 # 飛昇的碼農

    通常程式不會直接去操作CPU核心執行緒,而是透過核心執行緒的介面輕量級程序(LWP)來操作的,也就是通常意義上的執行緒.

    系統在執行多執行緒任務時,資料儲存在RAM中,然而每個執行緒都有一個本地快取,也就是CPU快取,並不會每次都從RAM讀取資料,所以就會出現執行緒不安全的情況。

    Java中volatile關鍵字主要是用來修飾變數使其能夠被執行緒可見.

  • 4 # 樂淘學程式設計

    https://m.toutiaocdn.com/group/6682538998579069453/?app=news_article&timestamp=1556016557&req_id=201904231849160100160441948694FA8&group_id=6682538998579069453

    參考這篇文章

  • 5 # 熙爸愛釣魚

    非java程式設計師,不過volatile在其他語言中也存在,簡單說下。

    1,volatile只在多執行緒程式中有意義。

    2,為了提高效能,編譯器工作時會進行一些最佳化,如指令排序,甚至跳過一些指令。如:

    var a=1;

    a=2;

    a=3;

    編譯後的結果可能就只執行 a = 3

    3,程式執行時,普通變數會有快取機制(如cpu快取、執行緒本地快取等),程式讀取時先從快取讀取,所以多執行緒的程式執行時可能存在髒讀問題。即第一個執行緒已經修改了變數值,但第二個執行緒還在使用快取中的舊資料。

    volatile的作用就是告訴編譯器,不要對使用該變數的程式碼進行最佳化,每次讀寫操作都訪問變數的原始資料。

  • 6 # 強哥教你學程式設計

    volatile在Java語言中扮演者重要的角色,它具有可見性以及禁止指令重排序兩個非常顯著的特點,要想解釋清楚volatile的用法,首先我們要對Java的記憶體模型JMM有一個非常熟悉的瞭解,所以我從以下幾點來分析volatile。

    一、Java記憶體模型JMM

    Java的記憶體模型規定:所有的變數都儲存在主記憶體中,每一個執行緒都有屬於自己的工作記憶體,當讀取主記憶體的變數時,執行緒的工作記憶體都會都會儲存這個變數的副本,執行緒對變數的操作都是在自己的工作記憶體中,在適當的時候會把自己工作記憶體的變數同步到主記憶體中。

    從上面的內容中可以得出一個結論,多執行緒對變數的修改,都是先修改自己的工作記憶體的變數,然後把工作記憶體中修改的在適當的時候同步到主記憶體中,那麼問題就來了,適當的時候是什麼時候呢?不確定,所以就有問題了,當主記憶體中有一個變數i=0,假如同時有兩個執行緒去修改i的值,當執行緒1讀取主記憶體中的i=1,然後複製一份副本在自己的工作記憶體中,然後i=1,但是這是操作的自己的工作記憶體i=1,但是這個i=1什麼時候重新整理到主記憶體中呢?剛才我們說了,不確定,此時執行緒二讀取主存的變數i=0,然後也複製一份到自己的工作記憶體中,然後i=2,然後在適當的時候重新整理到主存中,所以最終的結果可能是執行緒二i=2的結果先重新整理到主存中,執行緒一i=1最後重新整理到主存中,這就導致現在主存中i=1,所以與想象的結果不一樣。

    二、volatile的大白話

    瞭解了Java的記憶體模型JMM,我們瞭解了對於一個共享變數,如果有多個執行緒併發的修改這個共享變數,最終得到的結果可能與我們想象的不太一樣,這是由於JMM的機制導致的,而這和我們所說的volatile有什麼關係的,那接下來我們就說說。

    結論:1:如果一個變數被volatile修飾,那麼它在工作記憶體中修改的變數會立刻被重新整理到主存中。而不是上面所說的不確定的時候

    2:如果讀取一個被volatile修飾的變數,會把此執行緒工作記憶體中的此變數副本置為無效,它會從主記憶體中重新讀取這個變數到自己的工作記憶體。

    上面這兩點分別是volatile寫記憶體語義和volatile記憶體語義。

    三、volatile在JDK的使用

    在JDK中,併發包中volatile把它的特點發揮到了極致,尤其透過框架AQS的state就是被volatile修飾的,在加上CAS構建出了無鎖化的同步框架,在ConcurrentHashMap中也是因為有了volatile的作用加上CAS操作提高了很大的效能。

  • 中秋節和大豐收的關聯?
  • 國家電網網申一般什麼時候出來?