深入理解Volatile

2021-09-12 01:34:31 字數 3288 閱讀 3961

**:

一旦乙個共享變數(類的成員變數、 類的靜態成員變數) 被 volatile 修飾之後, 那麼就具備了兩層語義:

保證了不同執行緒對這個變數進行讀取時的可見性,即乙個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。 (volatile 解決了執行緒間共享變數的可見性問題)。

禁止進行指令重排序, 阻止編譯器對**的優化。

具體內容參考我的另外一篇部落格:

volatile 關鍵字禁止指令重排序有兩層意思:

為了實現 volatile 的記憶體語義, 加入 volatile 關鍵字時, 編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障, 會多出乙個 lock 字首指令。 記憶體屏障是一組處理器指令, 解決禁止指令重排序和記憶體可見性的問題。 編譯器和 cpu 可以在保證輸出結果一樣的情況下對指令重排序, 使效能得到優化。 處理器在進行重排序時是會考慮指令之間的資料依賴性。

記憶體屏障, 有 2 個作用:

lock 字首指令在多核處理器下會引發了兩件事情:

將當預處理器中這個變數所在快取行的資料會寫回到系統記憶體。 這個寫回記憶體的操作會引起在其他 cpu 裡快取了該記憶體位址的資料無效。 但是就算寫回到記憶體, 如果其他處理器快取的值還是舊的, 再執行計算操作就會有問題, 所以在多處理器下, 為了保證各個處理器的快取是一致的, 就會實現快取一致性協議, 每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了, 當處理器發現自己快取行對應的記憶體位址被修改, 就會將當預處理器的快取行設定成無效狀態, 當處理器要對這個資料進行修改操作的時候, 會強制重新從系統記憶體裡把資料讀到處理器快取裡。

它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置, 也不會把前面的指令排到記憶體屏障的後面; 即在執行到記憶體屏障這句指令時, 在它前面的操作已經全部完成。

當程式在執行過程中, 會將運算需要的資料從主存複製乙份到 cpu 的快取記憶體當中, 那麼 cpu 進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料, 當運算結束之後, 再將快取記憶體中的資料重新整理到主存當中。 舉個簡單的例子, 比如下面的這段**:

i = i+1複製**
當執行緒執行這個語句時, 會先從主存當中讀取 i 的值, 然後複製乙份到快取記憶體當中, 然後 cpu 執行指令對 i 進行加 1 操作, 然後將資料寫入快取記憶體,最後將快取記憶體中 i 最新的值重新整理到主存當中。這個**在單執行緒中執行是沒有任何問題的, 但是在多執行緒中執行就會有問題了。 在多核 cpu 中, 每條執行緒可能執行於不同的 cpu 中, 因此每個執行緒執行時有自己的快取記憶體(對單核 cpu 來說, 其實也會出現這種問題, 只不過是以執行緒排程的形式來分別執行的) 。

本文我們以多核 cpu 為例比如同時有 2 個執行緒執行這段**, 假如初始時 i 的值為 0, 那麼我們希望兩個執行緒執行完之後 i 的值變為 2。 但是事實會是這樣嗎?

可能存在下面一種情況: 初始時, 兩個執行緒分別讀取 i 的值存入各自所在的cpu 的快取記憶體當中, 然後執行緒 1 進行加 1 操作, 然後把 i 的最新值 1 寫入到記憶體。 此時執行緒 2 的快取記憶體當中 i 的值還是 0, 進行加 1 操作之後, i 的值為1, 然後執行緒 2 把 i 的值寫入記憶體。最終結果 i 的值是 1, 而不是 2。 這就是著名的快取一致性問題。 通常稱這種被多個執行緒訪問的變數為共享變數。也就是說, 如果乙個變數在多個 cpu 中都存在快取(一般在多執行緒程式設計時才會出現) , 那麼就可能存在快取不一致的問題。

如何解決快取一致性的問題:為了解決快取不一致性問題, 通常來說有以下 2 種解決方法:1) 通過在匯流排加 lock#鎖的方式2) 通過快取一致性協議

通過在匯流排加 lock#鎖的方式:在早期的 cpu 當中, 是通過在匯流排上加 lock#鎖的形式來解決快取不一致的問題。 因為 cpu 和其他部件進行通訊都是通過匯流排來進行的, 如果對匯流排加 lock#鎖的話, 也就是說阻塞了其他 cpu 對其他部件訪問(如記憶體) ,從而使得只能有乙個 cpu 能使用這個變數的記憶體。 比如上面例子中 如果乙個執行緒在執行 i = i +1, 如果在執行這段**的過程中, 在匯流排上發出了 lcok#鎖的訊號, 那麼只有等待這段**完全執行完畢之後, 其他 cpu 才能從變數 i所在的記憶體讀取變數, 然後進行相應的操作。 這樣就解決了快取不一致的問題。但是上面的方式會有乙個問題, 由於在鎖住匯流排期間, 其他 cpu 無法訪問記憶體, 導致效率下。

但是上面的方式會有乙個問題, 由於在鎖住匯流排期間, 其他 cpu 無法訪問記憶體, 導致效率低下。

通過快取一致性協議:所以就出現了快取一致性協議。 該協議保證了每個快取中使用的共享變數的副本是一致的。

它核心的思想是: 當 cpu 向記憶體寫入資料時, 如果發現操作的變數是共享變數, 即在其他 cpu 中也存在該變數的副本, 會發出訊號通知其他 cpu 將該變數的快取行置為無效狀態, 因此當其他 cpu 需要讀取這個變數時, 發現自己快取中快取該變數的快取行是無效的, 那麼它就會從記憶體重新讀取。

這裡用一張圖來詳細分析指令的執行順序:

storestore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了,因為storestore屏障將保障上面所有的普通寫在volatile寫之前重新整理到主記憶體

x86處理器僅僅會對寫-讀操作做重排序

因此會省略掉讀-讀、讀-寫和寫-寫操作做重排序的記憶體屏障

在x86中,jmm僅需在volatile後面插入乙個storeload屏障即可正確實現volatile寫-讀的記憶體語義

這意味著在x86處理器中,volatile寫的開銷比volatile讀的大,因為storeload屏障開銷比較大

/* 

* 一、volatile 關鍵字:當多個執行緒進行操作共享資料時,可以保證記憶體中的資料可見。

* 相較於 synchronized 是一種較為輕量級的同步策略。

* 注意:

* 1. volatile 不具備「互斥性」

* 2. volatile 不能保證變數的「原子性」

*/

public class testvolatile

} ​ }

​}

​class threaddemo implements runnable catch (interruptedexception e)

flag = true;

system.out.println("flag=" + isflag());

} public boolean isflag

()

public void setflag(boolean flag)

} 複製**

深入理解volatile關鍵字

併發的三大性質 併發分析的切入點分為兩個核心,三大性質 public class volatiledemo thread.start try catch interruptedexception e isover true volatile修飾的共享變數進行寫操作時,會多出lock字首的指令,實現快...

深入理解C語言 深入理解指標

關於指標,其是c語言的重點,c語言學的好壞,其實就是指標學的好壞。其實指標並不複雜,學習指標,要正確的理解指標。指標也是一種變數,占有記憶體空間,用來儲存記憶體位址 指標就是告訴編譯器,開闢4個位元組的儲存空間 32位系統 無論是幾級指標都是一樣的 p操作記憶體 在指標宣告時,號表示所宣告的變數為指...

mysql 索引深入理解 深入理解MySql的索引

為什麼索引能提高查詢速度 先從 mysql的基本儲存結構說起 mysql的基本儲存結構是頁 記錄都存在頁裡邊 各個資料頁可以組成乙個雙向鍊錶每個資料頁中的記錄又可以組成乙個單向鍊錶 每個資料頁都會為儲存在它裡邊兒的記錄生成乙個頁目錄,在通過主鍵查詢某條記錄的時候可以在頁目錄中使用二分法快速定位到對應...