Java 記憶體模型

weiweiyi發表於2022-02-28

在多核時代,如何提高CPU的效能成為了一個永恆的話題,而這個話題的討論主要就是如何定義一個高效能的記憶體模型,記憶體模型用於定義處理器的各層快取與共享記憶體的同步機制及執行緒和記憶體互動的規則。

Java的世界也有屬於它自己的記憶體模型,Java記憶體模型,即Java Memory Model,簡稱JMM。由於Java被定義成一種跨平臺的語言,所以在記憶體的描述上面也要能是跨平臺的,Java虛擬機器試圖定義一種統一的記憶體模型,能將各種底層硬體及作業系統的記憶體訪問差異進行封裝,使Java程式在不同硬體及作業系統上都能達到相同的併發效果。它描述了程式中各個變數之間的關係,包括例項域、靜態域、資料元素及在實際計算機系統中將變數儲存到記憶體和從記憶體中取出變數的底層細節。

為更好理解JMM的工作機制,由圖帶入,從整體上看有幾個比較重要的主體,主存、工作記憶體、變數、變數副本、執行緒等。首先看主存與工作記憶體及他們的關係,主存儲存了java程式的所有變數,當然這個變數不包括區域性變數和方法引數,而工作記憶體則包含了這些變數的副本;其次是執行緒與工作記憶體的關係,每個執行緒都有一個屬於自己的工作記憶體,不同執行緒之間的工作記憶體是互相不可見的,且執行緒對變數的操作也只能是針對自己的工作記憶體;最後是關於執行緒之間的通訊機制,由於執行緒之間不可直接傳遞,假如一條執行緒對一個變數進行重新賦值,那麼只能通過如下途徑讓另外一條執行緒知道,執行緒一將變數改變反應到主存中,執行緒二再從主存中讀取,這樣就基本完成了執行緒之間的通訊了。

Java 記憶體模型

JMM定義了八個操作來完成工作記憶體與主存的通訊。假如一條執行緒準備對一個變數進行新的賦值操作,它可能會先用lock操作鎖住主存中的某個變數(不讓其他執行緒獲得此變數的鎖,直至使用unlock操作釋放該變數的鎖),接著使用read操作將變數從主存獨到工作記憶體,緊接著load操作將得到的變數值放到工作記憶體中的變數副本,use操作則將變數值傳給執行緒執行引擎進行運算操作,assign操作把新的變數值從執行緒執行引擎中傳遞到工作記憶體,繼續往下,store操作則把變數值從工作記憶體傳送到主存中,接著write操作將得到的值寫入主存相應的變數中,最後使用unlock操作釋放變數的鎖。

Java記憶體模型具有三個特性:原子性、可見性和有序性。

  • 原子性,java記憶體模型保證了read、load、assign、use、store、write六個操作具有原子性,我們可以認為除了long和double型別外,對其他基本資料型別所對應的記憶體單元的訪問讀寫都是原子的。但由於這個原子性的顆粒度太小,通常情況下我們需要更大顆粒度的原子性,這時就需要用鎖來保證了。
  • 可見性,在java記憶體模型中,簡單說如果一條執行緒更改了共享變數的值,而其他執行緒能馬上知道這個更改,我們則說這個變數具有可見性。一般來說有四種方式能保證變數的可見性,分別為volatile、synchronized、final和鎖。首先談談volatile,被此關鍵詞宣告的變數,每當有任何更改時都將立即同步到主存中,而每個執行緒要使用這個變數時都要重新從主存重新整理到工作記憶體,這樣就確保了變數的可見性(當然,普通變數最終也會同步到主存,再由主存同步到每個執行緒的工作記憶體,只是這個最終可能比較“長久”,不能保證可見性);由於synchronized底層也是通過鎖進行實現,所以synchronized和鎖的本質是一樣的,當一個執行緒釋放一個鎖時,將會強制重新整理工作記憶體中的變數值到主存中,而當另一個執行緒獲取此鎖的時候將會強制重新裝載此變數值,當然這兩個執行緒獲取的是同一個鎖,這樣就保證了變數的可見性;被final宣告的變數一旦完成初始化,其他執行緒就能看到這個final變數。其實,可見性其實可以看成是一種機制,執行緒在進入/退出同步塊程式時,它將傳送/接收一個變數的更改。

JMM可以說是Java的基礎,它的定義將直接影響JVM及java多執行緒實現的機制,要想深入瞭解多執行緒併發中的相關問題現象,對Java記憶體模型的深入研究是必不可少的。它的定義必須考慮下面幾個方面,其一是如何更加有效地提高執行緒的效能效率;其二是如何將底層物理硬體及作業系統的差異遮蔽掉提供統一的對外概念;最後是如何使它的模型既嚴謹又寬鬆,保證語義不會產生歧義和一些優化擴充套件。

====廣告時間,可直接跳過====

鄙人的新書《Tomcat核心設計剖析》已經在京東預售了,有需要的朋友可以到 item.jd.com/12185360.ht… 進行預定。感謝各位朋友。

=========================

歡迎關注:

Java 記憶體模型

相關文章