1. 程式人生 > >Java基礎-記憶體模型

Java基礎-記憶體模型

併發程式設計中的兩個問題:執行緒之間如何通訊及執行緒之間如何同步。

通訊是指執行緒之間以何種機制來交換資訊。在指令式程式設計中,執行緒之間的通訊有兩種,共享記憶體和訊息傳遞。

在共享記憶體的併發模型中,執行緒之間共享程式的公共狀態,通過寫-讀記憶體中的公共狀態進行隱式通訊。在訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過傳送訊息來顯式進行通訊。

同步是指程式中用於控制不同執行緒間操作發生相對順序的機制。在共享記憶體併發模型裡,同步是顯式進行的。程式設計師必須顯式地指定某個方法或某段程式碼需要線上程之間互斥執行。在訊息傳遞的併發模型裡,由於訊息的傳送必須在訊息的接收之前,因此同步是隱式進行的。

Java的併發採用的是共享記憶體模型,Java執行緒之間的通訊總是隱式的進行,整個通訊過程對程式設計師完全透明。

一、Java記憶體模型的抽象結構

在Java中,所有例項域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體線上程之間共享。區域性變數,方法定義引數和異常處理不會線上程之間共享,他們不會有記憶體可見性問題,也不受記憶體模型的影響。

Java執行緒之間的通訊由Java記憶體模型(簡稱JMM)控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體,本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體時JMM的一個抽象概念,並不真實存在。他涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器優化。Java記憶體模型的抽象如下:

如果執行緒1和執行緒2通訊:執行緒1把本地記憶體x1中更新過的共享變數重新整理到主記憶體中去;執行緒2到主記憶體中去讀取執行緒1之前更新過的共享變數。

二、從原始碼到指令序列的重排序

在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序。重排序分為3種。

1)編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。

2)指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。

3)記憶體系統的重排序。用於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

原始碼——》1編譯器優化重排——》2指令級並行重排序——》3記憶體系統重排序——》最終執行的指令序列

對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

JMM屬於語言級的記憶體模型,他確保在不同的編譯器和不同的處理器平臺上,通過禁止特定型別的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證。

三、happens-before

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。這裡提到的兩個操作可以是在一個執行緒之間,也可以是在不同的執行緒之間。與程式設計師相關的happens-before規則如下:

程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作

監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。

傳遞性:如果A happens-before B,且B happens-before C,那麼A  happens-before C.

兩個操作之間具有happens-before關係,並不意味著前一個操作必須要在後一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前。

一個happens-before規則對應於一個或多個編譯器重排序規則。對於Java程式設計師來說,happens-before規則簡單易懂,它避免Java程式設計師為了理解JMM提供的記憶體可見性保證而去學習複雜的重排序規則及這些規則的具體實現方法。