Java記憶體模型以及執行緒安全的可見性問題
- 2019 年 10 月 7 日
- 筆記
Java記憶體模型 VS JVM運行時數據區
首先Java記憶體模型(JMM)和JVM運行時數據區並不是一個東西,許多介紹Java記憶體模型的文章描述的堆,方法區,Java虛擬機棧,本地方法棧,程式計數器這東西並不是Java記憶體模型的內容而是JVM運行時數據區的內容。 要理解二者的區別就要了解《Java虛擬機規範》和《Java語言規範》。我們知道Java虛擬機上並不知只有Java語言,像JRuby, ,Scala,Kotlin,Groovy等也都運行在Java虛擬機上,而這些語言想要在Java虛擬機上運行就要遵守《Java虛擬機規範》,而JVM運行時數據區就是《Java虛擬機規範》的內容。而《Java語言規範》就只是針對Java語言的規範,它對Java記憶體模型做了詳細的描述。

什麼是Java記憶體模型(JMM)?
要了解Java記憶體模型,首先要了解什麼是記憶體模型,之間在CPU快取和記憶體屏障 中我們了解到快取一致性問題以及處理器優化的指令重排序問題。為了保證並發編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——記憶體模型。它解決了 CPU 多級快取、處理器優化、指令重排等導致的記憶體訪問問題,保證了並發場景下的一致性、原子性和有序性。而Java記憶體模型就是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體數據不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題的一種規範。目的是保證並發編程場景中的原子性、可見性和有序性。 Java記憶體模型可以分為執行緒棧(或者叫工作記憶體,它是每個執行緒所獨有的)和堆(或者叫主記憶體,與JVM運行時數據區的堆並不是一個概念,它是所執行緒共享的),其大致邏輯圖如下:

JMM中的具體內容
Shared Variables定義
可以在執行緒之間共享的記憶體稱為共享記憶體或堆記憶體 所有實例欄位,靜態欄位和數組元素都存儲在共享記憶體,這些欄位和數組就是共享變數 衝突:如果至少有一個訪問是寫操作,那麼對同一個變數的兩次訪問是衝突的
這些能被多個執行緒訪問的共享變數是記憶體模型規範的對象
執行緒間操作
執行緒間操作指一個執行緒執行的操作可被其他執行緒感知或被其他執行緒直接影響 Java記憶體模型只描述執行緒間操作,不描述執行緒內操作,執行緒內操作按照執行緒內語義執行 執行緒間操作有:
- read操作(一般讀,即非volatile讀)
- write操作(一般寫,即非volatile寫)
- volatile read
- volatile write
- Lock,Unlock
- 執行緒的第一個和最後一個操作
- 外部操作
對同步規則的定義
- 對volatile變數V的寫入,與所有其它執行緒後續對V的讀同步
- 對於監視器m的解鎖與所有後續操作對於m的加鎖同步
- 對於每個屬性寫入默認值(0,false, null)與每個執行緒對其進行的操作同步
- 啟動執行緒的操作與執行緒中的第一個操作同步
- 執行緒T2的最後操作與執行緒T1發現T2已經結束同步
- 如果執行緒T1中斷了T2,那麼執行緒T1的中斷操作與其他所有執行緒發現T2被中斷了同步
happens-before先行發生原則
happens-before關係用於描述兩個有衝突的動作之間的順序,如果一個action happens before 另一個action,則第一個操作對第二個操作可見,JVM需要實現如下happens-before規則:
- 某個執行緒中的每個動作都happens-before該執行緒中該動作後面的操作
- 某個管程中的unlock動作happens-before同一個管程上後續的lock操作
- 對某個volatile欄位的寫操作happens-before每個後續對該volatile欄位的讀操作
- 在某個對象上調用start()方法happens-before被啟動執行緒的任意動作
- 如果在執行緒t1中成功執行了t2.join(),則t2中的所有操作對t1可見
- 如果某個動作a happens-before動作b,且b happens-before動作c,則a happens-before c
final在JMM中的處理
final在該對象的構造函數中設置對象的欄位,當執行緒看到該對象時,將始終看到該對象的final欄位的正確構造版本。如果在構造函數中設置欄位後發生讀取,則會看到該final欄位分配的值,否則它將看到默認值。讀取該對象的final成員變數之前,先要讀取共享對象。 通常被 static final修飾的欄位, 不能被修改。然而System.in, System.out, System.err被static final修飾卻可以修改,遺留問題,必須通過set方法改變,我們將這些欄位稱為防寫,以區別於普通final欄位。
Word Tearing位元組處理
有些處理器(尤其是早期的Alphas處理器)沒有提供寫單個位元組的功能。在這樣的處理器上更新byte數組,若只是簡單的讀取整個內容,更新對應的位元組,然後將整個內容再寫回記憶體,將是不合法的。這個問題有時候被稱為「字分裂(word tearing)」,更新位元組有難度的處理器,就需要尋求其他方式來解決。因此,編程人員需要注意,盡量不要對byte[]中的元素進行重新賦值,更不要在多執行緒中這樣做。
可見性問題
可見性:主要是指一個執行緒對共享變數的寫入可以被後續另一個執行緒讀取到,也就說一個執行緒對共享變數的操作對另一個執行緒是可見的。 而可見性問題就是指一個執行緒對共享變數進行了寫入而其他的執行緒卻無法讀取到該執行緒寫入的結果,根據以下工作記憶體的快取的模型我們可以知道,造成可見性的問題主要有兩方面,一個是數據在寫入的時候只是寫入了快取而沒有寫入主記憶體,一個是數據在讀取的時候只是從快取中讀取到了數據而沒有從主記憶體讀取數據。

可見性問題的解決方法 — volatile關鍵字
volatile關鍵字可以保證一個執行緒對共享變數的修改,能夠及時的被其他執行緒看到。 根據JMM中的happen before 和同步原則:
- 對某個volatile欄位的寫操作happens-before每個後續對該volatile欄位的讀操作
- 對volatile變數V的寫入,與所有其它執行緒後續對V的讀同步 而要滿足這些條件volatile關鍵字就具有以下功能:
- 禁止快取,volatile變數的訪問控制符會加個ACC_VOLATILE,《Java虛擬機規範》 中的對它的描述就是「cannot be cached」
- 對volatile變數相關的指令不做重排序