1. 程式人生 > >【Java併發】二、JVM記憶體模型

【Java併發】二、JVM記憶體模型

JVM記憶體模型

什麼是Java記憶體模型

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

想要理解Java的併發機制,就必須先了解JMM,JMM的關鍵技術點都是圍繞多執行緒的原子性、可見性和有序性建立起來的。

執行緒之間的通訊

想要控制執行緒之間的狀態和先後順序,執行緒之間需要通訊,當自己佔有臨界資源時,告知別的執行緒掛起;當自己離開臨界區時,喚醒別的執行緒。

  • 共享記憶體:執行緒之間共享程式的公共狀態,執行緒之間通過讀寫公共狀態來隱式通訊,比如通過共享物件通訊;
  • 訊息傳遞:執行緒之間沒有公共狀態,執行緒之間必須通過明確的訊息傳送來進行顯示通訊,比如Java裡的wait()notify()執行緒之間的通訊(thread signal

執行緒之間的同步

同步是指在需要競爭的情況下,程式用於控制不同執行緒之間操作發生相對順序的機制。

  • 在共享記憶體併發模型中,同步是顯式的。程式必須指定在何處需要執行緒互斥,比如Java中的同步方法、同步程式碼塊等
  • 在訊息傳遞的併發模型中,訊息的傳送和接收本來就有先後順序,因此同步是隱式的。

JAVA的記憶體模型

Java的併發採用的是共享記憶體模型,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。執行緒本地記憶體和主記憶體之間的抽象關係為:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體,本地記憶體中儲存了主記憶體中共享變數的副本,執行緒中的讀寫操作都是對副本的操作,JMM決定何時將副本替換到主記憶體中。

原子性

原子操作是不可中斷的,也就是說是互斥的,即使是多個執行緒一起執行同一個原子操作,一旦某個執行緒已經開始,就不會受到別的執行緒的干擾。

指令重排

  • 編譯器優化的重排:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。比如對不同變數的賦值操作,雖然在單執行緒中對不同變數賦值的結果是順序無關的,編譯器可以任意指定順序,但是在多執行緒中,多個執行緒對同一個共享變數賦值,就會導致變數的結果出現無法預測的情況;
  • 指令並行的重排:現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性(即後一個執行的語句無需依賴前面執行的語句的結果),處理器可以改變語句對應的機器指令的執行順序。CPU指令是按流水線技術來執行的,但是一旦因為某些原因出現流水終端,比如運算元還沒準備好,那麼所有硬體裝置就會停止一個或者多個週期,會造成CPU效能下降,指令重排可以優化這種中斷,可以在中斷時把後面與這個指令無關的指令先執行,等到下一個週期再執行中斷的指令,如果中斷的指令還沒有準備好,那繼續執行後面的指令,直到中斷的指令可以執行。單執行緒的情況下沒有問題,因為可以確定各個指令線上程內是否相關,但是對於多執行緒的情況,無法確定指令間是否相關,因此可能導致錯誤的執行順序;
  • 記憶體系統的重排:由於處理器使用快取和讀寫快取衝區,這使得載入(load)和儲存(store)操作看上去可能是在亂序執行,因為三級快取的存在,導致記憶體與快取的資料同步存在時間差。

可見性

可見性指的是當一個執行緒修改了某個共享變數的值,其他執行緒是否能夠馬上得知這個修改的值。單執行緒沒有可見性的說法,但是多執行緒因為上面的指令重排、Java的共享變數操作機制等,可能會導致執行混亂,一個執行緒取到的主記憶體共享變數值可能並不是最新的值,各個執行緒中的本地記憶體副本對別的執行緒並不可見,因為存在本地記憶體和主記憶體同步的延遲,可見性也是一個問題

有序性

和可見性類似,單執行緒的程式碼執行都是按照程式執行順序來的,但因為指令重排、主記憶體和本地記憶體的同步延遲,導致多執行緒之間的執行順序看起來是無序的。

JMM的解決方案

  • JVM自身提供了一套對基本資料型別讀寫操作的原子性;
  • synchronizedReentrantLock保證方法或程式碼塊級別的原子性;
  • volatilesynchronized可以保證共享變數的可見性,synchronized是通過原子性保證讀寫操作之間沒有延遲,但volatile修飾的共享變數是保證該 共享變數對所有執行緒是可見的,當一個執行緒要讀取共享變數時,JMM會強行讓執行緒內本地記憶體中的副本無效,直接到主記憶體中讀取;當一個執行緒要寫共享變數時,JMM會把對應的本地記憶體副本值刷入主記憶體當中。此外volatile變數還可以禁止指令重排,但不能保證原子性。
  • happens-before原則保證多執行緒環境下兩個操作間的原子性、可見性以及有序性,如果處處都需要通過顯示地指定,那開發將非常麻煩,JMM有一個happens-before來輔助保證程式執行的原子性、可見性以及有序性的問題:
    • 程式順序原則,即在一個執行緒內必須保證語義序列性,也就是說按照程式碼順序執行。
    • 鎖規則 解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說,如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)。
    • volatile規則 volatile變數的寫,先發生於讀,這保證了volatile變數的可見性,簡單的理解就是,volatile變數在每次被執行緒訪問時,都強迫從主記憶體中讀該變數的值,而當該變數發生變化時,又會強迫將最新的值重新整理到主記憶體,任何時刻,不同的執行緒總是能夠看到該變數的最新值。
    • 執行緒啟動規則 執行緒的start()方法先於它的每一個動作,即如果執行緒A在執行執行緒B的start方法之前修改了共享變數的值,那麼當執行緒B執行start方法時,執行緒A對共享變數的修改對執行緒B可見
    • 傳遞性 A先於B ,B先於C 那麼A必然先於C
    • 執行緒終止規則 執行緒的所有操作先於執行緒的終結,Thread.join()方法的作用是等待當前執行的執行緒終止。假設線上程B終止之前,修改了共享變數,執行緒A從執行緒B的join方法成功返回後,執行緒B對共享變數的修改將對執行緒A可見。
    • 執行緒中斷規則 對執行緒 interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測執行緒是否中斷。
    • 物件終結規則 物件的建構函式執行,結束先於finalize()方法

記憶體屏障

真正實現禁止指令重排的,是記憶體屏障,它的作用是在兩個指令間插入一條Memory Barrier,告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier重排,也就保證了順序執行。