轉(zhuǎn)自:https://juejin.im/editor/drafts/5acda6976fb9a028d937821f
一旦一個共享變量(類的成員變量、 類的靜態(tài)成員變量) 被 volatile 修飾之后, 那么就具備了兩層語義:
保證了不同線程對這個變量進行讀取時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。 (volatile 解決了線程間共享變量的可見性問題)。
禁止進行指令重排序, 阻止編譯器對代碼的優(yōu)化。
內(nèi)存可見性:
第一: 使用 volatile 關(guān)鍵字會強制將修改的值立即寫入主存;
第二: 使用 volatile 關(guān)鍵字的話, 當線程 2 進行修改時, 會導致線程 1 的工作內(nèi)存中緩存變量 stop 的緩存行無效(反映到硬件層的話, 就是 CPU 的 L1或者 L2 緩存中對應的緩存行無效) ;
第三: 由于線程 1 的工作內(nèi)存中緩存變量 stop 的緩存行無效, 所以線程 1再次讀取變量 stop 的值時會去主存讀取。
那么, 在線程 2 修改 stop 值時(當然這里包括 2 個操作, 修改線程 2 工作內(nèi)存中的值, 然后將修改后的值寫入內(nèi)存) , 會使得線程 1 的工作內(nèi)存中緩存變量 stop 的緩存行無效, 然后線程 1 讀取時, 發(fā)現(xiàn)自己的緩存行無效, 它會等待緩存行對應的主存地址被更新之后, 然后去對應的主存讀取最新的值。
禁止重排序:
volatile 關(guān)鍵字禁止指令重排序有兩層意思:
當程序執(zhí)行到 volatile 變量的讀操作或者寫操作時, 在其前面的操作的更改肯定全部已經(jīng)進行, 且結(jié)果已經(jīng)對后面的操作可見; 在其后面的操作肯定還沒有進行
在進行指令優(yōu)化時, 不能把 volatile 變量前面的語句放在其后面執(zhí)行,也不能把 volatile 變量后面的語句放到其前面執(zhí)行。
為了實現(xiàn) volatile 的內(nèi)存語義, 加入 volatile 關(guān)鍵字時, 編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障, 會多出一個 lock 前綴指令。 內(nèi)存屏障是一組處理器指令, 解決禁止指令重排序和內(nèi)存可見性的問題。 編譯器和 CPU 可以在保證輸出結(jié)果一樣的情況下對指令重排序, 使性能得到優(yōu)化。 處理器在進行重排序時是會考慮指令之間的數(shù)據(jù)依賴性。
內(nèi)存屏障, 有 2 個作用:
1.先于這個內(nèi)存屏障的指令必須先執(zhí)行, 后于這個內(nèi)存屏障的指令必須后執(zhí)行。
2.使得內(nèi)存可見性。 所以, 如果你的字段是 volatile, 在讀指令前插入讀屏障, 可以讓高速緩存中的數(shù)據(jù)失效, 重新從主內(nèi)存加載數(shù)據(jù)。 在寫指令之后插入寫屏障, 能讓寫入緩存的最新數(shù)據(jù)寫回到主內(nèi)存。
Lock 前綴指令在多核處理器下會引發(fā)了兩件事情:
將當前處理器中這個變量所在緩存行的數(shù)據(jù)會寫回到系統(tǒng)內(nèi)存。 這個寫回內(nèi)存的操作會引起在其他 CPU 里緩存了該內(nèi)存地址的數(shù)據(jù)無效。 但是就算寫回到內(nèi)存, 如果其他處理器緩存的值還是舊的, 再執(zhí)行計算操作就會有問題, 所以在多處理器下, 為了保證各個處理器的緩存是一致的, 就會實現(xiàn)緩存一致性協(xié)議, 每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了, 當處理器發(fā)現(xiàn)自己緩存行對應的內(nèi)存地址被修改, 就會將當前處理器的緩存行設(shè)置成無效狀態(tài), 當處理器要對這個數(shù)據(jù)進行修改操作的時候, 會強制重新從系統(tǒng)內(nèi)存里把數(shù)據(jù)讀到處理器緩存里。
它確保指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置, 也不會把前面的指令排到內(nèi)存屏障的后面; 即在執(zhí)行到內(nèi)存屏障這句指令時, 在它前面的操作已經(jīng)全部完成。
深入理解緩存一致性問題:
當程序在運行過程中, 會將運算需要的數(shù)據(jù)從主存復制一份到 CPU 的高速緩存當中, 那么 CPU 進行計算時就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù), 當運算結(jié)束之后, 再將高速緩存中的數(shù)據(jù)刷新到主存當中。 舉個簡單的例子, 比如下面的這段代碼:
i = i+1
當線程執(zhí)行這個語句時, 會先從主存當中讀取 i 的值, 然后復制一份到高速緩存當中, 然后 CPU 執(zhí)行指令對 i 進行加 1 操作, 然后將數(shù)據(jù)寫入高速緩存,最后將高速緩存中 i 最新的值刷新到主存當中。這個代碼在單線程中運行是沒有任何問題的, 但是在多線程中運行就會有問題了。 在多核 CPU 中, 每條線程可能運行于不同的 CPU 中, 因此每個線程運行時有自己的高速緩存(對單核 CPU 來說, 其實也會出現(xiàn)這種問題, 只不過是以線程調(diào)度的形式來分別執(zhí)行的) 。
本文我們以多核 CPU 為例比如同時有 2 個線程執(zhí)行這段代碼, 假如初始時 i 的值為 0, 那么我們希望兩個線程執(zhí)行完之后 i 的值變?yōu)?2。 但是事實會是這樣嗎?
可能存在下面一種情況: 初始時, 兩個線程分別讀取 i 的值存入各自所在的CPU 的高速緩存當中, 然后線程 1 進行加 1 操作, 然后把 i 的最新值 1 寫入到內(nèi)存。 此時線程 2 的高速緩存當中 i 的值還是 0, 進行加 1 操作之后, i 的值為1, 然后線程 2 把 i 的值寫入內(nèi)存。最終結(jié)果 i 的值是 1, 而不是 2。 這就是著名的緩存一致性問題。 通常稱這種被多個線程訪問的變量為共享變量。也就是說, 如果一個變量在多個 CPU 中都存在緩存(一般在多線程編程時才會出現(xiàn)) , 那么就可能存在緩存不一致的問題。
如何解決緩存一致性的問題:為了解決緩存不一致性問題, 通常來說有以下 2 種解決方法:1) 通過在總線加 LOCK#鎖的方式2) 通過緩存一致性協(xié)議
通過在總線加 LOCK#鎖的方式:在早期的 CPU 當中, 是通過在總線上加 LOCK#鎖的形式來解決緩存不一致的問題。 因為 CPU 和其他部件進行通信都是通過總線來進行的, 如果對總線加 LOCK#鎖的話, 也就是說阻塞了其他 CPU 對其他部件訪問(如內(nèi)存) ,從而使得只能有一個 CPU 能使用這個變量的內(nèi)存。 比如上面例子中 如果一個線程在執(zhí)行 i = i +1, 如果在執(zhí)行這段代碼的過程中, 在總線上發(fā)出了 LCOK#鎖的信號, 那么只有等待這段代碼完全執(zhí)行完畢之后, 其他 CPU 才能從變量 i所在的內(nèi)存讀取變量, 然后進行相應的操作。 這樣就解決了緩存不一致的問題。但是上面的方式會有一個問題, 由于在鎖住總線期間, 其他 CPU 無法訪問內(nèi)存, 導致效率下。
但是上面的方式會有一個問題, 由于在鎖住總線期間, 其他 CPU 無法訪問內(nèi)存, 導致效率低下。
通過緩存一致性協(xié)議:所以就出現(xiàn)了緩存一致性協(xié)議。 該協(xié)議保證了每個緩存中使用的共享變量的副本是一致的。
它核心的思想是: 當 CPU 向內(nèi)存寫入數(shù)據(jù)時, 如果發(fā)現(xiàn)操作的變量是共享變量, 即在其他 CPU 中也存在該變量的副本, 會發(fā)出信號通知其他 CPU 將該變量的緩存行置為無效狀態(tài), 因此當其他 CPU 需要讀取這個變量時, 發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的, 那么它就會從內(nèi)存重新讀取。
內(nèi)存屏障可以被分為以下幾種類型:
LoadLoad 屏障: 對于這樣的語句 Load1; LoadLoad; Load2, 在 Load2 及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前, 保證 Load1 要讀取的數(shù)據(jù)被讀取完畢。
StoreStore 屏障: 對于這樣的語句 Store1; StoreStore; Store2, 在 Store2 及后續(xù)寫入操作執(zhí)行前, 保證 Store1 的寫入操作對其它處理器可見。
LoadStore 屏障: 對于這樣的語句 Load1; LoadStore; Store2, 在 Store2 及后續(xù)寫入操作被刷出前, 保證 Load1 要讀取的數(shù)據(jù)被讀取完畢。
StoreLoad 屏障: 對于這樣的語句 Store1; StoreLoad; Load2, 在 Load2 及后續(xù)所有讀取操作執(zhí)行前, 保證 Store1 的寫入對所有處理器可見。 它的開銷是四種屏障中最大的。 在大多數(shù)處理器的實現(xiàn)中, 這個屏障是個萬能屏障, 兼具其它三種內(nèi)存屏障的功能。
這里用一張圖來詳細分析指令的執(zhí)行順序:
StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對任意處理器可見了,因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存
x86處理器僅僅會對寫-讀操作做重排序
因此會省略掉讀-讀、讀-寫和寫-寫操作做重排序的內(nèi)存屏障
在x86中,JMM僅需在volatile后面插入一個StoreLoad屏障即可正確實現(xiàn)volatile寫-讀的內(nèi)存語義
這意味著在x86處理器中,volatile寫的開銷比volatile讀的大,因為StoreLoad屏障開銷比較大
對于Volatile的使用,附上代碼講解:
/**
* 一、volatile 關(guān)鍵字:當多個線程進行操作共享數(shù)據(jù)時,可以保證內(nèi)存中的數(shù)據(jù)可見。
* 相較于 synchronized 是一種較為輕量級的同步策略。
* 注意:
* 1\. volatile 不具備“互斥性”
* 2\. volatile 不能保證變量的“原子性”
*/
public class TestVolatile {//main線程和ThreadDemo線程對flag屬性的可見性問題
?
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
?
while(true){
if(td.isFlag()){
System.out.println("------------------");
break;
}
}
?
}
?
}
?
class ThreadDemo implements Runnable {
?
//不添加volatile將不會輸出--------------------
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}