本文是InfoQ上的《深入理解Java內存模型》 java-memory-model 系列文章彙總列表。同時列舉了每篇文章的主要內容:
深入理解Java內存模型(一)——基礎
http://www.infoq.com/cn/articles/java-memory-model-1
--內容摘要:
重排序
從java源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
處理器重排序與內存屏障指令
爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM把內存屏障指令分爲下列四類:
屏障類型 | 指令示例 | 說明 |
LoadLoad Barriers | Load1; LoadLoad; Load2 | 確保Load1數據的裝載,之前於Load2及所有後續裝載指令的裝載。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 確保Store1數據對其他處理器可見(刷新到內存),之前於Store2及所有後續存儲指令的存儲。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 確保Load1數據裝載,之前於Store2及所有後續的存儲指令刷新到內存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 確保Store1數據對其他處理器變得可見(指刷新到內存),之前於Load2及所有後續裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令)完成之後,才執行該屏障之後的內存訪問指令。 |
happens-before
從JDK5開始,java使用新的JSR -133內存模型(本文除非特別說明,針對的都是JSR- 133內存模型)。JSR-133提出了happens-before的概念,通過這個概念來闡述操作之間的內存可見性。
深入理解Java內存模型(二)——重排序
http://www.infoq.com/cn/articles/java-memory-model-2
--內容摘要:
數據依賴性
數據依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
寫後讀 | a = 1;b = a; | 寫一個變量之後,再讀這個位置。 |
寫後寫 | a = 1;a = 2; | 寫一個變量之後,再寫這個變量。 |
讀後寫 | a = b;b = 1; | 讀一個變量之後,再寫這個變量。 |
編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操作的執行順序。
as-if-serial語義
as-if-serial語義的意思指:不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵守as-if-serial語義。
爲了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作可能被編譯器和處理器重排序。
重排序對多線程的影響
一個重排序的例子。在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
深入理解Java內存模型(三)——順序一致性
http://www.infoq.com/cn/articles/java-memory-model-3
--內容摘要:
數據競爭與順序一致性保證
JMM對正確同步的多線程程序的內存一致性做了如下保證:
如果程序是正確同步的,程序的執行將具有順序一致性(sequentially
consistent)--即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同(馬上我們將會看到,這對於程序員來說是一個極強的保證)。這裏的同步是指廣義上的同步,包括對常用同步原語(lock,volatile和final)的正確使用。
深入理解Java內存模型(四)——volatile
http://www.infoq.com/cn/articles/java-memory-model-4--內容摘要:
volatile的特性
簡而言之,volatile變量自身具有下列特性:
- 可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
- 原子性:對任意單個volatile變量的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性。
volatile寫-讀建立的happens before關係
volatile寫-讀的內存語義
volatile寫的內存語義如下:
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
volatile讀的內存語義如下:
- 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
volatile內存語義的實現
下面,讓我們來看看JMM如何實現volatile寫/讀的內存語義。
前文我們提到過重排序分爲編譯器重排序和處理器重排序。爲了實現volatile內存語義,JMM會分別限制這兩種類型的重排序類型。下面是JMM針對編譯器制定的volatile重排序規則表:
是否能重排序 | 第二個操作 | ||
第一個操作 | 普通讀/寫 | volatile讀 | volatile寫 |
普通讀/寫 | NO | ||
volatile讀 | NO | NO | NO |
volatile寫 | NO | NO |
爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對於編譯器來說,發現一個最優佈置來最小化插入屏障的總數幾乎不可能,爲此,JMM採取保守策略。下面是基於保守策略的JMM內存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的後面插入一個StoreLoad屏障。
- 在每個volatile讀操作的後面插入一個LoadLoad屏障。
- 在每個volatile讀操作的後面插入一個LoadStore屏障。
上述volatile寫和volatile讀的內存屏障插入策略非常保守。在實際執行時,只要不改變volatile寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。
深入理解Java內存模型(五)——鎖
http://www.infoq.com/cn/articles/java-memory-model-5
--內容摘要:
鎖的釋放-獲取建立的happens before 關係
鎖釋放和獲取的內存語義
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須要從主內存中去讀取共享變量。
對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義,可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
鎖內存語義的實現
本文將藉助ReentrantLock的源代碼,來分析鎖內存語義的具體實現機制。本文把java(sun.misc.Unsafe類)的compareAndSet()方法調用簡稱爲CAS。
現在對公平鎖和非公平鎖的內存語義做個總結:
- 公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。
- 公平鎖獲取時,首先會去讀這個volatile變量。
- 非公平鎖獲取時,首先會用CAS更新這個volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義。
從本文對ReentrantLock的分析可以看出,鎖釋放-獲取的內存語義的實現至少有下面兩種方式:
- 利用volatile變量的寫-讀所具有的內存語義。
- 利用CAS所附帶的volatile讀和volatile寫的內存語義。
concurrent包的實現
由於java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:
- A線程寫volatile變量,隨後B線程讀這個volatile變量。
- A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
- A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
- A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。
如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
- 首先,聲明共享變量爲volatile;
- 然後,使用CAS的原子條件更新來實現線程之間的同步;
- 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下: