前言
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 |
---|---|---|---|---|
可見性 | 能夠保證 | 能夠保證 | 能夠保證 | 能夠保證 |
原子性 | 沒法保證 | 能夠保證 | 能夠保證 | 能夠保證 |
有序性 | 必定程度保證 | 能夠保證 | 能夠保證 | 沒法保證 |
關於我
這裏是阿星,一個熱愛技術的Java程序猿,公衆號 「程序猿阿星」 裏將會按期分享操做系統、計算機網絡、Java、分佈式、數據庫等精品原創文章,2021,與您在 Be Better 的路上共同成長!。
很是感謝各位小哥哥小姐姐們能 看到這裏,原創不易,文章有幫助能夠「點個贊」或「分享與評論」,都是支持(莫要白嫖)!
願你我都能奔赴在各自想去的路上,咱們下篇文章見!