漫畫:什么是 volatile 關鍵字?
722
2025-04-03
內存模型
概念
內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程的抽象。
Java內存模型
Java虛擬機規范中試圖定義一種Java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
主內存和與工作內存
主內存
Java內存模型規定了所有的變量都保存在主內存中。
工作內存
每個線程有自己的工作內存,保存了被該線程使用到的變量的主內存副本拷貝。
主內存和工作內存的關系
線程對變量的所有操作都必須在自己的工作內存中進行,而不能直接讀寫主內存中的變量。
不同的線程之間無法直接訪問對方工作內存中的變量。
線程間變量值的傳遞均需要通過主內存來完成。
內存間交互操作
Java定義了以下8中操作來完成變量與主內存中間的操作,如變量如何從主內存中拷貝到工作內存、如何從工作內存同步回主內存等。
Java虛擬機實現時必須保證下面提及的每一種操作都是原子的、不可再分的(double和long類型的變量允許有例外)。
lock:鎖定,作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態。
unlock:解鎖,作用于主內存的變量,它把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read:讀取,作用于主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用。
load:載入,作用于工作內存中的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
use:使用,作用于工作內存中的變量,它把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
assign:賦值,作用于工作內存中的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,沒到虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store:存儲,作用于工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
write:寫入,作用于主內存中的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
注:如果想要把變量從主內存中復制到工作內存中,那么就要按順序執行read和load操作;如果是要把工作內存中的變量同步到主內存中,那么就要順序執行store和write操作。但是Java虛擬機只規定這兩個操作是必須按順序執行,其他的可以不用。而且,這兩組操作,不需要連續執行,比如可以這樣:
Java虛擬機還規定了以下8種基本操作時的規則:
不允許read和load、store和write單獨單一出現。即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起會寫但是豬內存不接受的情況出現。
不允許一個線程丟棄它最近的assign的操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
不允許一個線程無原因地把數據從線程的工作內存同步回主內存中。
一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化的變量。
一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才能被解鎖。
如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個唄其他線程鎖定住的變量。
對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)。
volatile
volatile可以說是Java虛擬機提供的最輕量級的同步機制。當一個變量唄聲名為volatile類型后,它將具備兩個特性:
保證此變量對所有線程的可見性,這里的可見性是指當一條線程修改了這個變量的值,新值對于其他線程來說是立即得知的。但是,Java的運算并非原子操作,導致volatile變量的運算在并發下一樣是不安全的。?即valatile只能保證可見性,所以通常我們還是需要使用synchronized或java.util.concurrent中的原子來來保證原子性。?除了以下兩種情況:
運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
變量不需要與其他的狀態變量共同參與不變約束。
例:
分析結果:
上面的例子可以保證當shutdown被調用時,能保證所有線程中執行doWork()方法立即停止。
禁止指令重排序優化,普通變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。而volatile關鍵字能夠避免此類情況的發生。
例,如下偽代碼:
代碼分析:
如果initialized沒有被volatile修飾,就可能由于指令重排序的優化,導致位于線程A中的最后一句代碼initialized=true提前執行。
總結:
volatile變量讀操作的性能消耗與普通變量并太大差別,但是寫操作可能會慢一些,因為它需要再本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
對于long和double型變量的特殊規則
允許虛擬機將沒有被volatile修飾的64位數據的讀寫操作劃分為兩次32位的操作來進行,也就是說多線程在調用沒有被volatile修飾的long或者double型變量的時候可能調到半個變量的數值。但是這種情況非常罕見,因為Java虛擬機可以選擇把這兩個操作實現為具有原子性操作,而且基本上虛擬機也就這么做了。
原子性、可見性和有序性
原子性:由Java內存模型來直接保證的原子性操作包括read、load、assign、use、store和write,我們大致可以認為基本數據類型的訪問讀寫是具備原子性的。
可見性:指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取錢從主內存刷新變量值這種依賴主內存作傳遞媒介的方式來實現可見性的。 普通變量和volatile修飾的變量是一樣的,只是volatile保證了新值能夠立即同步到主內存,每次使用前立即從主內存刷新。
synchronized關鍵字保證可見性:對一個變量執行unlock之前,必須先把此變量同步回主內存中。
final關鍵字保證可見性:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把this的引用傳遞出去,那在其他線程中就能看見final字段的值。
有序性:總結一下,如果在本線程中,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。
先行發生原則
程序次序規則:在一個線程內,按照程序代碼順序(控制流順序及邏輯順序),前面的代碼比后面的代碼先行發生。
管程鎖定規則:一個unlock操作先行發生于后面對于同一個鎖的lock操作。
volatile變量規則:對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作。
線程啟動規則:Thread對象的start()方法先行發生于此線程的每一個動作。
線程終止規則:線程中的所有操作都先行發生于對此線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷時間的發生,可以通過Thread.interrupt()方法檢測到是否有中斷發生。
對象終結規則:一個對象的初始化完成先行發生于它的finalize()方法的開始。
傳遞性:如果操作A先行發生于操作B,操作B先行發生于操作C,那就說明操作A先行發生于操作C。
本文轉載自微信公眾號【java學習之道】。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。