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

參考文檔:html

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

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

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

1、什麼是重排序函數

重排序分爲2種優化

  • 編譯期指令重排

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

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

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

as-if-serial語義:.net

全部的動做均可覺得了優化而被重排序,可是必須保證它們重排序後的結果和程序代碼自己的應有結果是一致的。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

2、什麼是內存可見性

可見性:一個線程對共享變量值的修改,可以及時地被其餘線程看到
共享變量:若是一個變量在多個線程的工做內存中都存在副本,那麼這個變量就是這幾個線程的共享變量

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只能保證可見性,沒法保證原子性

 

3、內存屏障

內存屏障的做用:

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

硬件層的內存屏障分爲兩種: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值)

4、優化屏障

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

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

相關文章
相關標籤/搜索