如何徹底理解volatile關鍵字?
謝謝邀請~!下面從 用法、注意事項、底層原理進行說明!
JMM 基礎-計算機原理Java 內存模型即Java Memory Model,簡稱JMM,JMM定義了Java 虛擬機(JVM)在計算機(RAM) 中的工作方式。 JVM 是整個計算機虛擬模型,所以JMM是隸屬于JVM的,
在計算機系統中,寄存器是L0級緩存,接著依次是L1,L2,L3(接下來是內存,本地磁盤,遠程存儲).越往上的緩存存儲空間越小,速度越快,成本也越高。越往下的存儲空間越大,速度更慢,成本也越低。
從上至下,每一層都都可以是看作是更下一層的緩存,即:L0寄存器是L1一級緩存的緩存,
L1是L2的緩存,一次類推;每一層的數據都是來至于它的下一層。
在現在CPU上,一般來說L0,L1,L2,L3都繼承在CPU內部,而L1還分為一級數據緩存和一級指令緩存,分別用于存放數據和執行數據的指令解碼,每個核心擁有獨立的運算處理單元、控制器、寄存器、L1緩存、L2 緩存,然后一個CPU的多個核心共享最后一層CPU緩存L3。
CPU 的緩存一致性解決方案分為以下兩種方案
總線鎖(每次鎖總線,是悲觀鎖)
緩存鎖(只鎖緩存的數據)
MESI協議如下:
M(modify):I(invalid)E(Exclusive)S(Share)JMM內存模型的八種同步操作1、read(讀取),從主內存讀取數據
2、load(載入):將主內存讀取到的數據寫入到工作內存
3、use(使用): 從工作內存讀取數據來計算
4、assign(賦值):將計算好的值重新賦值到工作內存中
5、store(存儲):將工作內存數據寫入主內存
6、write(寫入):將store過去的變量值賦值給主內存中的變量
7、lock(鎖定):將主內存變量加鎖,標識為線程 獨占狀態
8、unlock(解鎖):將主內存變量解鎖,解鎖后其他線程可以鎖定該變量
Java 內存模型帶來的問題1、可見性問題
左邊CPU中運行的線程從主內存中拷貝對象obj到它的CPU緩存,把對象obj的count變量改為2,但這個變更對運行在右邊的CPU中的線程是不可見,因為這個更改還沒有flush到主內存中。
在多線程環境下,如果某個線程首次讀取共享變量,則首先到主內存中獲取該變量,然后存入到工作內存中,以后只需要在工作內存中讀取該變量即可,同樣如果對該變量執行了修改的操作,則先將新值寫入工作內存中,然后再刷新至于內存中,但是什么時候最新的值會被刷新到主內存中是不太確定的,一般來說是很快的,但是具體時間未知,,要解決共享對象可見性問題,我們可以使用volatile關鍵字或者加鎖。
2、競爭問題
線程A 和 線程B 共享一個對象obj, 假設線程A從主存讀取obj.count變量到自己的緩存中,同時,線程B也讀取了obj.count變量到它的CPU緩存,并且這兩個線程都對obj.count做了加1操作,此時,obj.count加1操作被執行了兩次,不過都在不同的CPU緩存中。
如果則兩個加1操作是串行執行的,那么obj.count變量便會在原始值上加2,最終主內存中obj.count的值會為3,然后圖中兩個加1操作是并行的,不管是線程A還是線程B先flush計算結果到主存,最終主存中的obj.count只會增加1次變成2,盡管一共有兩次加1操作,要解決上面的問題我們可以使用synchronized 代碼塊。
3、重排序
除了共享內存和工作內存帶來的問題,還存在重排序的問題,在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。
重排序分3中類型:
(1) 編譯器優化的重排序。
(2) 指令級并行的重排序
(3)內存系統的重排序
① 數據依賴性
數據依賴性: 如果兩個操作訪問同一變量,且這兩個操作中有一個為寫,此時這兩個操作之間就存在數據依賴性。
依賴性分為以下三種:
上圖很明顯,A和C存在數據依賴,B和C也存在數據依賴,而A和B之間不存在數據依賴,如果重排序了A和C或者B和C的執行順序,程序的執行結果就會被改變。
很明顯,不管如何重排序,都必須保證代碼在單線程下的運行正確,連單線程下都無法保證,更不用討論多線程并發的情況,所以就提出一個as - if -serial 的概念。
4、as - if -serial
意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as - if -serial 語義。
A和C之間存在數據依賴,同時B和C之間也存在數據依賴關系,因此在最終執行的指令序列中,C不能被重排序A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關系,編譯器和處理器可以重排序A和B之間的執行順序。
as - if -serial 語義把單線程程序保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器可以讓我們感覺到: 單線程程序看起來是按程序的順序來執行的。as-if-srial語義使單線程程序無需擔心重排序干擾他們,也無需擔心內存可見性的問題。
5、內存屏障
Java 編譯器在生成指令序列的適當位置會插入內存屏障來禁止特定類型的處理去重排序,從而讓程序按我們預想的流程去執行。
① 保證特定操作的執行順序
② 影響某些數據(或者是某條指令的執行結果)的內存可見性
編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化性能。插入一條Memory Barrier 會告訴編譯器和CPU ,不管什么指令都不能和這條Memory Barrier 指令重排序。
Memory Barrier 所做的另外一件事是強制刷出各種CPU cache, 如一個Write-Barrier(寫入屏障)將刷出所在的Barrier 之前寫入cache的數據,因此,任何CPU上的線程都能讀取到這些數據的最新版本。
JMM把內存屏障指令分為4類:
StoreLoad Barriers 是一個"全能型"的屏障,它同時具有其他3個屏障的效果,
volatile 關鍵字介紹1、保證可見性
對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫。
我們先看下面代碼:
initFlag 沒有用volatile關鍵字修飾;
上面結果為:
說明一個線程改變initFlag狀態,另外一個線程看不見;
如果加上volatile關鍵字呢?
結果如下:
我們通過匯編看下代碼的最終底層實現:
volatile寫的內存語義如下:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
比如:
如果我們將flag變量以volatile關鍵字修飾,那么實際上:線程A在寫flag變量后,本地內存A中被線程A更新過的兩個共享變量的值都被刷新到主內存中。
在讀flag變量后,本地內存B包含的值已經被置為無效。此時,線程B必須從主內存中讀取共享變量。線程B的讀取操作將導致本地內存B與主內存中的共享變量的值變成一致。
如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀線程B讀一個volatile變量后,寫線程A在寫這個volatile變量之前所有可見的共享變量的值都將立即變得對讀線程B可見。
2、原子性
volatile 不保證變量的原子性;
運行結果如下:
因為count ++;
包含 三個操作:
(1) 讀取變量count
(2) 將count變量的值加1
(3) 將計算后的值再賦給變量count
從JMM內存分析:
下面從字節碼分析為什么i++這種的用volatile修改不能保證原子性?
javap : 字節碼查看
其實i++這種操作主要可以分為3步:(匯編)
讀取volatile變量值到local增加變量的值把local的值寫回,讓其它的線程可見Load到store到內存屏障,一共4步,其中最后一步jvm讓這個最新的變量的值在所有線程可見,也就是最后一步讓所有的CPU內核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。
3、有序性
(1) volatile重排序規則表
① 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
② 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
③ 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
(2) volatile的內存屏障
① volatile寫
storestore屏障:對于這樣的語句store1; storestore; store2,在store2及后續寫入操作執行前,保證store1的寫入操作對其它處理器可見。(也就是說如果出現storestore屏障,那么store1指令一定會在store2之前執行,CPU不會store1與store2進行重排序)
storeload屏障:對于這樣的語句store1; storeload; load2,在load2及后續所有讀取操作執行前,保證store1的寫入對所有處理器可見。(也就是說如果出現storeload屏障,那么store1指令一定會在load2之前執行,CPU不會對store1與load2進行重排序
② volatile讀
在每個volatile讀操作的后面插入一個LoadLoad屏障。在每個volatile讀操作的后面插入一個loadstore屏障。
loadload屏障:對于這樣的語句load1; loadload; load2,在load2及后續讀取操作要讀取的數據被訪問前,保證load1要讀取的數據被讀取完畢。(也就是說,如果出現loadload屏障,那么load1指令一定會在load2之前執行,CPU不會對load1與load2進行重排序)
loadstore屏障:對于這樣的語句load1; loadstore; store2,在store2及后續寫入操作被刷出前,保證load1要讀取的數據被讀取完畢。(也就是說,如果出現loadstore屏障,那么load1指令一定會在store2之前執行,CPU不會對load1與store2進行重排序)
volatile的實現原理volatile的實現原理
? 通過對OpenJDK中的unsafe.cpp源碼的分析,會發現被volatile關鍵字修飾的變量會存在一個“lock:”的前綴。
? Lock前綴,Lock不是一種內存屏障,但是它能完成類似內存屏障的功能。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖。
? 同時該指令會將當前處理器緩存行的數據直接寫會到系統內存中,且這個寫回內存的操作會使在其他CPU里緩存了該地址的數據無效。
? 具體的執行上,它先對總線和緩存加鎖,然后執行后面的指令,最后釋放鎖后會把高速緩存中的臟數據全部刷新回主內存。在Lock鎖住總線的時候,其他CPU的讀寫請求都會被阻塞,直到鎖釋放。
【歡迎隨手關注@碼農的一天,希望對你有幫助】