小白也能看懂的Java記憶體模型
前言
Java併發程式設計可以說是中高階研發工程師的必備素養,也是面試必問,本文就是為了帶讀者們系統的一步一步擊破Java併發程式設計各個難點,打破屏障,在面試中所向披靡,拿到心儀的offer,Java併發程式設計系列文章依然採用圖文並茂的風格,讓小白也能秒懂。
Java記憶體模型(Java Memory Model
)簡稱J M M
,作為Java併發程式設計系列的開篇,它是Java併發程式設計的基礎知識,理解它能讓你更好的明白執行緒安全到底是怎麼一回事。
內容大綱
硬體記憶體模型
程式是指令與資料的集合,計算機執行程式時,是C P U
在執行每條指令,因為C P U
要從記憶體讀指令,又要根據指令指示去記憶體讀寫資料做運算,所以執行指令就免不了與記憶體打交道,早期記憶體讀寫速度與C P U
處理速度差距不大,倒沒什麼問題。
C P U快取
隨著C P U
技術快速發展,C P U
的速度越來越快,記憶體卻沒有太大的變化,導致記憶體的讀寫(IO
)速度與C P U
的處理速度差距越來越大,為了解決這個問題,引入了快取(Cache
)的設計,在C P U
與記憶體之間加上快取層,這裡的快取層就是指C P U
內的暫存器與快取記憶體(L1,L2,L3
)
從上圖中可以看出,暫存器最快,主內最慢,越快的儲存空間越小,離C P U
越近,相反儲存空間越大速度越慢,離C P U
越遠。
C P U如何與記憶體互動
C P U
執行時,會將指令與資料從主存複製到快取層,後續的讀寫與運算都是基於快取層的指令與資料,運算結束後,再將結果從快取層寫回主存。
上圖可以看出,C P U
基本都是在和快取層打交道,採用快取設計彌補主存與C P U
處理速度的差距,這種設計不僅僅體現在硬體層面,在日常開發中,那些併發量高的業務場景都能看到,但是凡事都有利弊,快取雖然加快了速度,同樣也帶來了在多執行緒場景存在的快取一致性問題,關於快取一致性問題後面會說,這裡大家留個印象。
Java記憶體模型
Java記憶體模型(Java Memory Model,J M M
),後續都以J M M
簡稱,J M M
是建立在硬體記憶體模型基礎上的抽象模型,並不是物理上的記憶體劃分,簡單說,為了使Java
虛擬機器(Java Virtual Machine,J V M
)在各平臺下達到一致的記憶體互動效果,需要遮蔽下游不同硬體模型的互動差異,統一規範,為上游提供統一的使用介面。
J M M
是保證J V M
在各平臺下對計算機記憶體的互動都能保證效果一致的機制及規範。
抽象結構
J M M
抽象結構劃分為執行緒本地快取與主存,每個執行緒均有自己的本地快取,本地快取是執行緒私有的,主存則是計算機記憶體,它是共享的。
不難發現J M M
與硬體記憶體模型差別不大,可以簡單的把執行緒類比成Core核心,執行緒本地快取類比成快取層,如下圖所示
雖然記憶體互動規範好了,但是多執行緒場景必然存線上程安全問題(競爭共享資源),為了使多執行緒能正確的同步執行,就需要保證併發的三大特性可見性、原子性、有序性。
可見性
當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改,這就是可見性,如果無法保證,就會出現快取一致性的問題,J M M
規定,所有的變數都放在主存中,當執行緒使用變數時,先從快取中獲取,快取未命中,再從主存複製到快取,最終導致執行緒操作的都是自己快取中的變數。
執行緒A執行流程
執行緒 A
從快取獲取變數a
快取未命中,從主存複製到快取,此時 a
是0
執行緒 A
獲取變數a
,執行計算計算結果 1
,寫入快取計算結果 1
,寫入主存
執行緒B執行流程
執行緒 B
從快取獲取變數a
快取未命中,從主存複製到快取,此時 a
是1
執行緒 B
獲取變數a,執行計算計算結果 2
,寫入快取計算結果 2
,寫入主存
A
、B
兩個執行緒執行完後,執行緒A
與執行緒B
快取資料不一致,這就是快取一致性問題,一個是1
,另一個是2
,如果執行緒A
再進行一次+1
操作,寫入主存的還是2
,也就是說兩個執行緒對a
共進行了3
次+1
,期望的結果是3
,最終得到的結果卻是2
。
解決快取一致性問題,就要保證可見性,思路也很簡單,變數寫入主存後,把其他執行緒快取的該變數清空,這樣其他執行緒快取未命中,就會去主存載入。
執行緒A執行流程
執行緒 A
從快取獲取變數a
快取未命中,從主存複製到快取,此時 a
是0
執行緒 A
獲取變數a
,執行計算計算結果 1
,寫入快取計算結果 1
,寫入主存,並清空執行緒B
快取a
變數
執行緒B執行流程
執行緒 B
從快取獲取變數a
快取未命中,從主存複製到快取,此時 a
是1
執行緒 B
獲取變數a,執行計算計算結果 2
,寫入快取計算結果 2
,寫入主存,並清空執行緒A
快取a
變數
A
、B
兩個執行緒執行完後,執行緒A
快取是空的,此時執行緒A再進行一次+1
操作,會從主存載入(先從快取中獲取,快取未命中,再從主存複製到快取)得到2
,最後寫入主存的是3
,Java
中提供了volatile
修飾變數保證可見性(本文重點是J M M
,所以不會對volatile
做過多的解讀)。
看似問題都解決了,然而上面描述的場景是建立在理想情況(執行緒有序的執行),實際中執行緒可能是併發(交替執行),也可能是並行,只保證可見性仍然會有問題,所以還需要保證原子性。
原子性
原子性是指一個或者多個操作在C P U
執行的過程中不被中斷的特性,要麼執行,要不執行,不能執行到一半,為了直觀的瞭解什麼是原子性,看看下面這段程式碼
int a=0;
a++;
原子性操作: int a=0
只有一步操作,就是賦值非原子操作: a++
有三步操作,讀取值、計算、賦值
如果多執行緒場景進行a++
操作,僅保證可見性,沒有保證原子性,同樣會出現問題。
併發場景(執行緒交替執行)
執行緒 A
讀取變數a
到快取,a
是0
進行 +1
運算得到結果1
切換到 B
執行緒B
執行緒執行完整個流程,a=1
寫入主存執行緒 A
恢復執行,把結果a=1
寫入快取與主存最終結果錯誤
並行場(執行緒同時執行)
執行緒 A
與執行緒B
同時執行,可能執行緒A
執行運算+1
的時候,執行緒B
就已經全部執行完成,也可能兩個執行緒同時計算完,同時寫入,不管是那種,結果都是錯誤的。
為了解決此問題,只要把多個操作變成一步操作,即保證原子性。
Java
中提供了synchronized
(同時滿足有序性、原子性、可見性)可以保證結果的原子性(注意這裡的描述),synchronized
保證原子性的原理很簡單,因為synchronized
可以對程式碼片段上鎖,防止多個執行緒併發執行同一段程式碼(本文重點是J M M
,所以不會對synchronized
做過多的解讀)。
併發場景(執行緒A
與執行緒B
交替執行)
執行緒 A
獲取鎖成功執行緒 A
讀取變數a
到快取,進行+1
運算得到結果1
此時切換到了 B
執行緒執行緒 B
獲取鎖失敗,阻塞等待切換回執行緒 A
執行緒 A
執行完所有流程,主存a=1
執行緒A釋放鎖成功,通知執行緒 B
獲取鎖執行緒B獲取鎖成功,讀取變數 a
到快取,此時a=1
執行緒B執行完所有流程,主存 a=2
執行緒B釋放鎖成功
並行場景
執行緒 A
獲取鎖成功執行緒 B
獲取鎖失敗,阻塞等待執行緒 A
讀取變數a
到快取,進行+1
運算得到結果1
執行緒 A
執行完所有流程,主存a=1
執行緒 A
釋放鎖成功,通知執行緒B
獲取鎖執行緒 B
獲取鎖成功,讀取變數a
到快取,此時a=1
執行緒 B
執行完所有流程,主存a=2
執行緒 B
釋放鎖成功
synchronized
對共享資原始碼段上鎖,達到互斥效果,天然的解決了無法保證原子性、可見性、有序性帶來的問題。
雖然在並行場A
執行緒還是被中斷了,切換到了B
執行緒,但它依然需要等待A
執行緒執行完畢,才能繼續,所以結果的原子性得到了保證。
有序性
在日常搬磚寫程式碼時,可能大家都以為,程式執行時就是按照編寫順序執行的,但實際上不是這樣,編譯器和處理器為了最佳化效能,會對程式碼做重排,所以語句實際執行的先後順序與輸入的程式碼順序可能一致,這就是指令重排序。
可能讀者們會有疑問“指令重排為什麼能最佳化效能?”,其實C P U
會對重排後的指令做並行執行,達到最佳化效能的效果。
重排序前的指令
重排序後的指令
重排序後,對a
操作的指令發生了改變,節省了一次Load a
和Store a
,達到效能最佳化效果,這就是重排序帶來的好處。
重排遵循as-if-serial
原則,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變執行結果(即不管怎麼重排序,單執行緒程式的執行結果不能被改變),下面這種情況,就屬於資料依賴。
int i = 10
int j = 10
//這就是資料依賴,int i 與 int j 不能排到 int c下面去
int c = i + j
但也僅僅只是針對單執行緒,多執行緒場景可沒這種保證,假設A、B
兩個執行緒,執行緒A
程式碼段無資料依賴,執行緒B
依賴執行緒A
的結果,如下圖(假設保證了可見性)
禁止重排場景(i預設0)
執行緒 A
執行i = 10
執行緒 A
執行b = true
執行緒 B
執行if( b )
透過驗證執行緒 B
執行i = i + 10
最終結果 i
是20
重排場景(i預設0)
執行緒 A
執行b = true
執行緒 B
執行if( b )
透過驗證執行緒 B
執行i = i + 10
執行緒 A
執行i = 10
最終結果 i
是10
為解決重排序,使用Java提供的volatile
修飾變數同時保證可見性、有序性,被volatile
修飾的變數會加上記憶體屏障禁止排序(本文重點是J M M
,所以不會對volatile
做過多的解讀)。
三大特性的保證
特性 | volatile | synchronized | Lock | Atomic |
---|---|---|---|---|
可見性 | 可以保證 | 可以保證 | 可以保證 | 可以保證 |
原子性 | 無法保證 | 可以保證 | 可以保證 | 可以保證 |
有序性 | 一定程度保證 | 可以保證 | 可以保證 | 無法保證 |
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2925891/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 小白也能看懂的JVM記憶體區域JVM記憶體
- promise初體驗,小白也能看懂Promise
- 小白也能看懂的MySQLMySql
- Java的記憶體模型Java記憶體模型
- Java 記憶體模型Java記憶體模型
- Java記憶體模型Java記憶體模型
- JVM記憶體結構、Java記憶體模型和Java物件模型JVM記憶體Java模型物件
- Java記憶體區域和記憶體模型Java記憶體模型
- Java記憶體模型FAQ(一) 什麼是記憶體模型Java記憶體模型
- JMM Java 記憶體模型Java記憶體模型
- Java記憶體模型-(1)Java記憶體模型
- Java物件記憶體模型Java物件記憶體模型
- 探索Java記憶體模型Java記憶體模型
- 理解Java記憶體模型Java記憶體模型
- 小白都能看得懂的java虛擬機器記憶體模型Java虛擬機記憶體模型
- java記憶體模型的實現Java記憶體模型
- Java記憶體模型的基礎Java記憶體模型
- Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題?Java記憶體模型
- 淺談JVM記憶體結構 和 Java記憶體模型 和 Java物件模型JVM記憶體Java模型物件
- 小白也能看懂的 AUC 曲線詳解
- Java記憶體模型 - 簡介Java記憶體模型
- Java記憶體模型簡介Java記憶體模型
- 淺談Java記憶體模型Java記憶體模型
- java記憶體模型——重排序Java記憶體模型排序
- Java記憶體模型之前奏Java記憶體模型
- Java基礎:記憶體模型Java記憶體模型
- java記憶體垃圾回收模型Java記憶體模型
- Java記憶體模型_基礎Java記憶體模型
- Java記憶體模型_重排序Java記憶體模型排序
- Java記憶體模型_volatileJava記憶體模型
- 同步和Java記憶體模型Java記憶體模型
- Java記憶體模型FAQ(五)舊的記憶體模型有什麼問題?Java記憶體模型
- 你瞭解Java記憶體模型麼(Java7、8、9記憶體模型的區別)Java記憶體模型
- 小白也能看懂的ArrayList的擴容機制
- Java併發中的記憶體模型Java記憶體模型
- JVM的藝術—JAVA記憶體模型JVMJava記憶體模型
- JAVA的記憶體模型及結構Java記憶體模型
- Java中的記憶體模型詳解Java記憶體模型