jvm(三)指令重排 & 內存屏障 & 可見性 & volatile & happen before


參考文檔:

https://tech.meituan.com/java-memory-reordering.html

http://0xffffff.org/2017/02/21/40-atomic-variable-mutex-and-memory-barrier/

內存可見性:http://blog.csdn.net/ty_laurel/article/details/52403718

一、什么是重排序

重排序分為2種

  • 編譯期指令重排

通過調整代碼中的指令順序,在不改變代碼語義的前提下,對變量訪問進行優化。從而盡可能的減少對寄存器的讀取和存儲,並充分復用寄存器。但是編譯器對數據的依賴關系判斷只能在單執行流內,無法判斷其他執行流對競爭數據的依賴關系

  • CPU亂序執行(Out-of-Order Execution)

流水線(Pipeline)和亂序執行是現代CPU基本都具有的特性。機器指令在流水線中經歷取指、譯碼、執行、訪存、寫回等操作。為了CPU的執行效率,流水線都是並行處理的,在不影響語義的情況下。處理器次序(Process Ordering,機器指令在CPU實際執行時的順序)和程序次序(Program Ordering,程序代碼的邏輯執行順序)是允許不一致的,即滿足As-if-Serial特性。顯然,這里的不影響語義依舊只能是保證指令間的顯式因果關系,無法保證隱式因果關系。即無法保證語義上不相關但是在程序邏輯上相關的操作序列按序執行

as-if-serial語義:

所有的動作都可以為了優化而被重排序,但是必須保證它們重排序后的結果和程序代碼本身的應有結果是一致的。Java編譯器、運行時和處理器都會保證單線程下的as-if-serial語義

       為保證as-if-serial語義,Java異常處理機制也會為重排序做一些特殊處理。例如在下面的代碼中,y = 0 / 0可能會被重排序在x = 2之前執行,為了保證最終不致於輸出x = 1的錯誤結果,JIT在重排序時會在catch語句中插入錯誤代償代碼,將x賦值為2,將程序恢復到發生異常時應有的狀態。這種做法的確將異常捕捉的邏輯變得復雜了,但是JIT的優化的原則是,盡力優化正常運行下的代碼邏輯,哪怕以catch塊邏輯變得復雜為代價,畢竟,進入catch塊內是一種“異常”情況的表現

public class Reordering {
    public static void main(String[] args) {
        int x, y;
        x = 1;
        try {
            x = 2;
            y = 0 / 0;    
        } catch (Exception e) {
        } finally {
            System.out.println("x = " + x);
        }
    }
}

重排序滿足happen before原則

  1. 程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)后執行的操作
  2. 管理鎖定規則:一個unlock操作happen—before后面(時間上的先后順序,下同)對同一個鎖的lock操作
  3. volatile變量規則:對一個volatile變量的寫操作happen—before后面對該變量的讀操作
  4. 線程啟動規則:Thread對象的start()方法happen—before此線程的每一個動作
  5. 線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行
  6. 線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生
  7. 對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始
  8. 傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C

二、什么是內存可見性

可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到
共享變量:如果一個變量在多個線程的工作內存中都存在副本,那么這個變量就是這幾個線程的共享變量

Java內存模型(JMM)
Java內存模型(Java Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取出變量這樣的底層細節。
    所有的變量都存儲在主內存中。每個線程都有自己獨立的工作內存,里面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝),如圖

兩條規定:

  • 線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接從主內存中讀取
  • 不同線程之間無法直接訪問其他線程工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。

在這種模型下會存在一個現象,即緩存中的數據與主內存的數據並不是實時同步的,各CPU(或CPU核心)間緩存的數據也不是實時同步的。這導致在同一個時間點,各CPU所看到同一內存地址的數據的值可能是不一致

 

如何實現內存可見性:

要實現共享變量的可見性,必須保證兩點

  • 線程修改后的共享變量值能夠及時從工作內存中刷新到主內存中
  • 其他線程能夠及時把共享變量的最新值從主內存更新到自己的工作內存中

1)synchronized實現可見性

 synchronized能夠實現:
    原子性(同步)
    可見性
JMM關於synchronized的兩條規定:

    • 線程解鎖前,必須把共享變量的最新值刷新到主內存中
    • 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主存中重新讀取最新的值

線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見
線程執行互斥代碼的過程

    1. 獲得互斥鎖
    2. 清空工作內存
    3. 從主內存拷貝變量的最新副本到工作內存
    4. 執行代碼
    5. 將更改后的共享變量的值刷新到主內存中
    6. 釋放互斥鎖

 2)volatile實現可見性

volatile關鍵字

              能夠保證volatile變量的可見性

             不能保證volatile變量復合操作的原子

       volatile如何實現內存的可見性:

    • 深入來說:通過加入內存屏障和禁止重排序優化來實現的

               在每個volatile寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障
               在每個volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障

    • 通俗地講:volatile變量在每次被線程訪問時,都強迫從主內存中重讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存。這樣任何時刻,不同的線程總能看到該變量的最新值。

線程寫volatile變量的過程:

    1. 改變線程工作內存中volatile變量副本的值
    2. 將改變后的副本的值從工作內存刷新到主內存

線程讀volatile變量的過程:

    1. 從主內存中讀取volatile變量的最新值到線程的工作內存中
    2. 從工作內存中讀取volatile變量的副本

synchronized vs volatile

  • volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程
  • synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性

 

三、內存屏障

內存屏障的作用:

  • 防止指令之間的重排序
  • 強制把寫緩沖區/高速緩存中的臟數據等寫回主內存,讓緩存中相應的數據失效

硬件層的內存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障

  • 對於Load Barrier來說,在指令前插入Load Barrier,可以讓高速緩存中的數據失效,強制從新從主內存加載數據
  • 對於Store Barrier來說,在指令后插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見

java內存屏障:

  • LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及后續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢
  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見
  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢
  • StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能

final語義中的內存屏障:

  • 新建對象過程中,構造體中對final域的初始化寫入和這個對象賦值給其他引用變量,這兩個操作不能重排序
  • 初次讀包含final域的對象引用和讀取這個final域,這兩個操作不能重排序(先賦值引用,再調用final值)

四、優化屏障

避免編譯器的重排序優化操作,保證編譯程序時在優化屏障之前的指令不會在優化屏障之后執行。這就保證了編譯時期的優化不會影響到實際代碼邏輯順序

優化屏障告知編譯器:
    內存信息已經修改,屏障后的寄存器的值必須從內存中重新獲取
    必須按照代碼順序產生匯編代碼,不得越過屏障


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM