新四季網

volatile原理(談談對Volatile的理解)

2023-05-28 15:06:43

帶著BAT大廠的面試問題去理解volatilevolatile關鍵字的作用是什麼?volatile能保證原子性嗎?之前32位機器上共享的long和double變量的為什麼要用volatile? 現在64位機器上是否也要設置呢?因為long和double操作可分為高32位和低32位兩部分,因此普通long或double類型讀/寫可能不是原子。因此,將共享long和double變量設置為volatile類型能保證任何情況下單次讀/寫操作的原子性。64位的要看jvm的具體實現。i 為什麼不能保證原子性?volatile是如何實現可見性的? 內存屏障。volatile是如何實現有序性的? happens-before等說下volatile的應用場景?Volatile是Java虛擬機提供的輕量級的同步機制(三大特性)保證可見性不保證原子性禁止指令重排被volatile修飾的共享變量,就具有了以下兩點特性:1 . 保證了不同線程對該變量操作的內存可見性;2 . 禁止指令重排序1.JMM是什麼

JMM是Java內存模型,也就是Java Memory Model,簡稱JMM,本身是一種抽象的概念,實際上並不存在,它描述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例欄位,靜態欄位和構成數組對象的元素)的訪問方式,JMM規定所有的共享變量都存儲於主內存,這裡所說的變量指的是實例變量和類變量,不包含局部變量,因為局部變量是線程私有的,因此不存在競爭問題。每一個線程還存在自己的工作內存,線程的工作內存,保留了被線程使用的變量的工作副本。線程對變量的所有的操作(讀,取)都必須在工作內存中完成,而不能直接讀寫主內存中的變量。不同線程之間也不能直接訪問對方工作內存中的變量,線程間變量的值的傳遞需要通過主內存中轉來完成。

JMM關於同步的規定:

線程解鎖前,必須把共享變量的值刷新回主內存線程解鎖前,必須讀取主內存的最新值,到自己的工作內存加鎖和解鎖是同一把鎖

由於JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(有些地方稱為棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫會主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲著主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程:

上面提到了兩個概念:主內存 和 工作內存

主內存:就是計算機的內存,也就是經常提到的8G內存,16G內存,當我們實例化 new student,那麼 age = 37 也是存儲在主內存中當有三個線程同時訪問 student中的age變量時,那麼每個線程都會拷貝一份,到各自的工作內存,從而實現了變量的拷貝,即:JMM內存模型的可見性,指的是當主內存區域中的值被某個線程寫入更改後,其它線程會馬上知曉更改後的值,並重新得到更改後的值。

1.1緩存一致性問題

緩存一致性的問題——就是當多個處理器運算任務都涉及到同一塊主內存區域的時候,將可能導致各自的緩存數據不一。

為了解決緩存一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議進行操作,這類協議主要有MSI、MESI等等。

1.1.1MESI(緩存行無效)

當CPU寫數據時,如果發現操作的變量是共享變量,即在其它CPU中也存在該變量的副本,會發出信號通知其它CPU將該內存變量的緩存行設置為無效,因此當其它CPU讀取這個變量的時,發現自己緩存該變量的緩存行是無效的,那麼它就會從內存中重新讀取。

1.1.2總線嗅探

為什麼主線程中某個值被更改後,其它線程能馬上知曉呢?其實這裡是用到了總線嗅探技術

總線嗅探技術——就是每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存值是否過期了,當處理器發現自己的緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置為無效狀態,當處理器對這個數據進行修改操作的時候,會重新從內存中把數據讀取到處理器緩存中。

1.1.3總線風暴

總線嗅探技術有哪些缺點?

由於Volatile的MESI緩存一致性協議,需要不斷的從主內存嗅探和CAS循環,無效的交互會導致總線帶寬達到峰值。因此不要大量使用volatile關鍵字,至於什麼時候使用volatile、什麼時候用鎖以及Syschonized都是需要根據實際場景的。

2.JMM的特性

JMM的三大特性,volatile只保證了兩個,即可見性和有序性,不滿足原子性、

當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存

當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,線程接下來將從主內存中讀取共享變量。

2.1可見性代碼驗證

當一個變量被volatile修飾時,那麼對它的修改會立刻刷新到主存,當其它線程需要讀取該變量時,會去內存中讀取新值。而普通變量則不能保證這一點。

其實通過synchronized和Lock也能夠保證可見性,線程在釋放鎖之前,會把共享變量值都刷回主存,但是synchronized和Lock的開銷都更大。

/** * Volatile Java虛擬機提供的輕量級同步機制 * * 可見性(及時通知) * 不保證原子性 * 禁止指令重排 * */import java.util.concurrent.TimeUnit;/** * 假設是主物理內存 */class MyData { int number = 0; public void addTo60 { this.number = 60; }}/** * 驗證volatile的可見性 * 1\. 假設int number = 0, number變量之前沒有添加volatile關鍵字修飾 */public class VolatileDemo { public static void main(String args []) { // 資源類 MyData myData = new MyData; // AAA線程 實現了Runnable接口的,lambda表達式 new Thread( -> { System.out.println(Thread.currentThread.getName "\t come in"); // 線程睡眠3秒,假設在進行運算 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace; } // 修改number的值 myData.addTo60; // 輸出修改後的值 System.out.println(Thread.currentThread.getName "\t update number value:" myData.number); }, "AAA").start; while(myData.number == 0) { // main線程就一直在這裡等待循環,直到number的值不等於零 } // 按道理這個值是不可能列印出來的,因為主線程運行的時候,number的值為0,所以一直在循環 // 如果能輸出這句話,說明AAA線程在睡眠3秒後,更新的number的值,重新寫入到主內存,並被main線程感知到了 System.out.println(Thread.currentThread.getName "\t mission is over"); /** * 最後輸出結果: * AAA come in * AAA update number value:60 * 最後線程沒有停止,並行沒有輸出 mission is over 這句話,說明沒有用volatile修飾的變量,是沒有可見性 */ }}

輸出結果:

最後線程沒有停止,並行沒有輸出 mission is over 這句話,說明沒有用volatile修飾的變量,是沒有可見性

當我們修改MyData類中的成員變量時,並且添加volatile關鍵字修飾

最後輸出結果為:

2.2原子性

Java中,對基本數據類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要麼就沒有執行。 比如:

i = 2;j = i;i ;i = i 1;

上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實吧,分為兩步,一是讀取i的值,然後再賦值給j,這就是2步操作了,稱不上原子操作,i 和i = i 1其實是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最後的值可能出現多種情況,就是因為滿足不了原子性。

這麼說來,只有簡單的讀取,賦值是原子操作,還只能是用數字賦值,用變量的話還多了一步讀取變量值的操作。有個例外是,虛擬機規範中允許對64位數據類型(long和double),分為2次32為的操作來處理,但是最新JDK實現還是實現了原子操作的。

舉例:

public class Test { public volatile int inc = 0; public void increase { inc ; } public static void main(String[] args) { final Test test = new Test; for(int i=0;i<10;i ){ new Thread{ public void run { for(int j=0;j1) //保證前面的線程都執行完 Thread.yield; System.out.println(test.inc); }}

按道理來說結果是10000,但是運行下很可能是個小於10000的值。因為這裡的操作inc 是個複合操作,包括讀取inc的值,對其自增,然後再寫回主存。

假設線程A,讀取了inc的值為10,這時候被阻塞了,因為沒有對變量進行修改,觸發不了volatile規則。

線程B此時也讀讀inc的值,主存裡inc的值依舊為10,做自增,然後立刻就被寫回主存了,為11。

此時又輪到線程A執行,由於工作內存裡保存的是10,所以繼續做自增,再寫回主存,11又被寫了一遍。所以雖然兩個線程執行了兩次increase,結果卻只加了一次。

有人說,volatile不是會使緩存行無效的嗎(1.1.1MESI)?但是這裡線程A讀取到線程B也進行操作之前,並沒有修改inc值,所以線程B讀取的時候,還是讀的10。

又有人說,線程B將11寫回主存,不會把線程A的緩存行設為無效嗎?但是線程A的讀取操作已經做過了啊,只有在做讀取操作時,發現自己緩存行無效,才會去讀主存的值,所以這裡線程A只能繼續做自增了。

綜上所述,在這種複合操作的情景下,原子性的功能是維持不了了。但是volatile在對變量的讀/寫操作都是單步的的例子裡,還是能保證原子性的。

要想保證原子性,只能藉助於synchronized,Lock以及並發包下的atomic的原子操作類了,即對基本數據類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。

2.3有序性

JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程序的執行結果不能改變。比如下面的程序段:

double pi = 3.14; //Adouble r = 1; //Bdouble s= pi * r * r;//C

上面的語句,可以按照A->B->C執行,結果為3.14,但是也可以按照B->A->C的順序執行,因為A、B是兩句獨立的語句,而C則依賴於A、B,所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序不會影響到單線程的執行,但是在多線程中卻容易出問題。

比如這樣的代碼:

int a = 0;bool flag = false;public void write { a = 2; //1 flag = true; //2}public void multiply { if (flag) { //3 int ret = a * a;//4 }}

假如有兩個線程執行上述代碼段,線程1先執行write,隨後線程2再執行multiply,最後ret的值一定是4嗎?結果不一定:

如圖所示,write方法裡的1和2做了重排序,線程1先對flag賦值為true,隨後執行到線程2,ret直接計算出結果,再到線程1,這時候a才賦值為2,很明顯遲了一步。

這時候可以為flag加上volatile關鍵字,禁止重排序,可以確保程序的「有序性」,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裡的代碼都是一次性執行完畢的。

另外,JMM具備一些先天的有序性,即不需要通過任何手段就可以保證的有序性,通常稱為happens-before原則。<>定義了如下happens-before規則:

程序順序規則: 一個線程中的每個操作,happens-before於該線程中的任意後續操作監視器鎖規則:對一個線程的解鎖,happens-before於隨後對這個線程的加鎖volatile變量規則: 對一個volatile域的寫,happens-before於後續對這個volatile域的讀傳遞性:如果A happens-before B ,且 B happens-before C, 那麼 A happens-before Cstart規則: 如果線程A執行操作ThreadB_start(啟動線程B) , 那麼A線程的ThreadB_starthappens-before 於B中的任意操作join原則: 如果A執行ThreadB.join並且成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join操作成功返回。interrupt原則: 對線程interrupt方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted方法檢測是否有中斷發生finalize原則:一個對象的初始化完成先行發生於它的finalize方法的開始

3.volatile底層的實現機制3.1 Lock指令

如果把加入volatile關鍵字的代碼和未加入volatile關鍵字的代碼都生成彙編代碼,會發現加入volatile關鍵字的代碼會多出一個lock前綴指令。

lock前綴指令實際相當於一個內存屏障,內存屏障提供了以下功能:

1 . 重排序時不能把後面的指令重排序到內存屏障之前的位置

2 . 使得本CPU的Cache寫入內存

3 . 寫入動作也會引起別的CPU或者別的內核無效化其Cache,相當於讓新寫入的值對別的線程可見。

在 Pentium 和早期的 IA-32 處理器中,lock 前綴會使處理器執行當前指令時產生一個 LOCK# 信號,會對總線進行鎖定,其它 CPU 對內存的讀寫請求都會被阻塞,直到鎖釋放。 後來的處理器,加鎖操作是由高速緩存鎖代替總線鎖來處理。 因為鎖總線的開銷比較大,鎖總線期間其他 CPU 沒法訪問內存。 這種場景多緩存的數據一致通過緩存一致性協議(MESI)來保證。

緩存一致性

緩存是分段(line)的,一個段對應一塊存儲空間,稱之為緩存行,它是 CPU 緩存中可分配的最小存儲單元,大小 32 字節、64 字節、128 字節不等,這與 CPU 架構有關,通常來說是 64 字節。 LOCK# 因為鎖總線效率太低,因此使用了多組緩存。 為了使其行為看起來如同一組緩存那樣。因而設計了 緩存一致性協議。 緩存一致性協議有多種,但是日常處理的大多數計算機設備都屬於 " 嗅探(snooping)" 協議。 所有內存的傳輸都發生在一條共享的總線上,而所有的處理器都能看到這條總線。 緩存本身是獨立的,但是內存是共享資源,所有的內存訪問都要經過仲裁(同一個指令周期中,只有一個 CPU 緩存可以讀寫內存)。 CPU 緩存不僅僅在做內存傳輸的時候才與總線打交道,而是不停在嗅探總線上發生的數據交換,跟蹤其他緩存在做什麼。 當一個緩存代表它所屬的處理器去讀寫內存時,其它處理器都會得到通知,它們以此來使自己的緩存保持同步。 只要某個處理器寫內存,其它處理器馬上知道這塊內存在它們的緩存段中已經失效。

3.2 volatile 有序性實現volatile 的 happens-before 關係

happens-before 規則中有一條是 volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意後續對這個 volatile 域的讀。

//假設線程A執行writer方法,線程B執行reader方法class VolatileExample { int a = 0; volatile boolean flag = false; public void writer { a = 1; // 1 線程A修改共享變量 flag = true; // 2 線程A寫volatile變量 } public void reader { if (flag) { // 3 線程B讀同一個volatile變量 int i = a; // 4 線程B讀共享變量 …… } }}

根據 happens-before 規則,上面過程會建立 3 類 happens-before 關係。

根據程序次序規則:1 happens-before 2 且 3 happens-before 4。根據 volatile 規則:2 happens-before 3。根據 happens-before 的傳遞性規則:1 happens-before 4。

因為以上規則,當線程 A 將 volatile 變量 flag 更改為 true 後,線程 B 能夠迅速感知。

volatile 禁止重排序

為了性能優化,JMM 在不改變正確語義的前提下,會允許編譯器和處理器對指令序列進行重排序。JMM 提供了內存屏障阻止這種重排序。Java 編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。

volatile寫是在前面和後面分別插入內存屏障,而volatile讀操作是在後面插入兩個內存屏障

JMM 會針對編譯器制定 volatile 重排序規則表:

" NO " 表示禁止重排序。

為了實現 volatile 內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

對於編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎是不可能的,為此,JMM 採取了保守的策略。

在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。在每個 volatile 寫操作的後面插入一個 StoreLoad 屏障。在每個 volatile 讀操作的後面插入一個 LoadLoad 屏障。在每個 volatile 讀操作的後面插入一個 LoadStore 屏障。

volatile 寫是在前面和後面分別插入內存屏障,而 volatile 讀操作是在後面插入兩個內存屏障。

內存屏障

說明

StoreStore 屏障

禁止上面的普通寫和下面的 volatile 寫重排序。

StoreLoad 屏障

防止上面的 volatile 寫與下面可能有的 volatile 讀/寫重排序。

LoadLoad 屏障

禁止下面所有的普通讀操作和上面的 volatile 讀重排序。

LoadStore 屏障

禁止下面所有的普通寫操作和上面的 volatile 讀重排序。

4.使用到volatile的兩個例子狀態量標記,就如上面對flag的標記

int a = 0;volatile bool flag = false;public void write { a = 2; //1 flag = true; //2}public void multiply { if (flag) { //3 int ret = a * a;//4 }}

這種對變量的讀寫操作,標記為volatile可以保證修改對線程立刻可見。比synchronized,Lock有一定的效率提升。

2.單例模式的實現,典型的雙重檢查鎖定(DCL)

相關文章->juejin.cn/post/684490…

class Singleton{ private volatile static Singleton instance = null; private Singleton { } public static Singleton getInstance { //第一次加鎖是為了性能考慮,因為線程安全發生在對象初始化,如果第一次不判斷相當於全局控制,造成浪費。 if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton; } } return instance; }}

這是一種懶漢的單例模式,使用時才創建對象

instance 採用 volatile 關鍵字修飾也是很有必要的, instance= new Singleton; 這段代碼其實是分為三步執行:

為 instance 分配內存空間初始化 instance將 instance 指向分配的內存地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getInstance 後發現 instance 不為空,因此返回 instance,但此時 instance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。

5.總結volatile修飾符適用於以下場景:某個屬性被多個線程共享,其中有一個線程修改了此屬性,其他線程可以立即得到修改後的值,比如booleanflag;或者作為觸發器,實現輕量級同步。volatile屬性的讀寫操作都是無鎖的,它不能替代synchronized,因為它沒有提供原子性和互斥性。因為無鎖,不需要花費時間在獲取鎖和釋放鎖_上,所以說它是低成本的。volatile只能作用於屬性,我們用volatile修飾屬性,這樣compilers就不會對這個屬性做指令重排序。volatile提供了可見性,任何一個線程對其的修改將立馬對其他線程可見,volatile屬性不會被線程緩存,始終從主 存中讀取。volatile提供了happens-before保證,對volatile變量v的寫入happens-before所有其他線程後續對v的讀操作。volatile可以使得long和double的賦值是原子的。volatile可以在單例雙重檢查中實現可見性和禁止指令重排序,從而保證安全性。,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了