Java 中 volatile 關鍵字是一個類型修飾符。JDK 1.5 之后,對其語義進行了增強。
- 保證了不同線程對共享變量進行操作時的可見性,即一個線程修改了共享變量的值,共享變量修改后的值對其他線程立即可見
- 通過禁止編譯器、CPU 指令重排序和部分 happens-before 規則,解決有序性問題
volatile 可見性的實現
- 在生成匯編代碼指令時會在 volatile 修飾的共享變量進行寫操作的時候會多出 Lock 前綴的指令
- Lock 前綴的指令會引起 CPU 緩存寫回內存
- 一個 CPU 的緩存回寫到內存會導致其他 CPU 緩存了該內存地址的數據無效
- volatile 變量通過緩存一致性協議保證每個線程獲得最新值
- 緩存一致性協議保證每個 CPU 通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是修改
- 當 CPU 發現自己緩存行對應的內存地址被修改,會將當前 CPU 的緩存行設置成無效狀態,重新從內存中把數據讀到 CPU 緩存
看一下我們之前的一個可見性問題的測試例子
package constxiong.concurrency.a014;
/**
* 測試可見性問題
* @author ConstXiong
*/
public class TestVisibility {
//是否停止 變量
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
//啟動線程 1,當 stop 為 true,結束循環
new Thread(() -> {
System.out.println("線程 1 正在運行...");
while (!stop) ;
System.out.println("線程 1 終止");
}).start();
//休眠 10 毫秒
Thread.sleep(10);
//啟動線程 2, 設置 stop = true
new Thread(() -> {
System.out.println("線程 2 正在運行...");
stop = true;
System.out.println("設置 stop 變量為 true.");
}).start();
}
}
程序會一直循環運行下去
這個就是因為 CPU 緩存導致的可見性導致的問題。
線程 2 設置 stop 變量為 true,線程 1 在 CPU 1上執行,讀取的 CPU 1 緩存中的 stop 變量仍然為 false,線程 1 一直在循環執行。
示意如圖:
給 stop 變量加上 valatile 關鍵字修飾就可以解決這個問題。
volatile 有序性的實現
-
3 個 happens-before 規則實現:
1) 對一個 volatile 變量的寫 happens-before 任意后續對這個 volatile 變量的讀
2) 在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在后面的操作
3) happens-before 傳遞性,A happens-before B,B happens-before C,則 A happens-before C
- 內存屏障(Memory Barrier 又稱內存柵欄,是一個 CPU 指令)禁止重排序
1) 在程序運行時,為了提高執行性能,在不改變正確語義的前提下,編譯器和 CPU 會對指令序列進行重排序。
2) Java 編譯器會在生成指令時,為了保證在不同的編譯器和 CPU 上有相同的結果,通過插入特定類型的內存屏障來禁止特定類型的指令重排序
3) 編譯器根據具體的底層體系架構,將這些內存屏障替換成具體的 CPU 指令
4) 內存屏障會告訴編譯器和 CPU:不管什么指令都不能和這條 Memory Barrier 指令重排序
內存屏障
- 為了實現 volatile 內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的 CPU 重排序。
- 對於編譯器,內存屏障將限制它所能做的重排序優化;對於 CPU,內存屏障將會導致緩存的刷新操作
- volatile 變量的寫操作,在變量的前面和后面分別插入內存屏障;volatile 變量的讀操作是在后面插入兩個內存屏障
1) 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障
2) 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障
3) 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障
4) 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障
- 屏障說明
1) StoreStore:禁止之前的普通寫和之后的 volatile 寫重排序;
2) StoreLoad:禁止之前的 volatile 寫與之后的 volatile 讀/寫重排序
3) LoadLoad:禁止之后所有的普通讀操作和之前的 volatile 讀重排序
4) LoadStore:禁止之后所有的普通寫操作和之前的 volatile 讀重排序
我覺得,有序性最經典的例子就是 JDK 並發包中的顯式鎖 java.util.concurrent.locks.Lock 的實現類對有序性的保障。
以下摘自:http://ifeve.com/java鎖是如何保證數據可見性的/
實現 Lock 的代碼思路簡化為
private volatile int state;
void lock() {
read state
if (can get lock)
write state
}
void unlock() {
write state
}
- 假設線程 a 通過調用lock方法獲取到鎖,此時線程 b 也調用了 lock() 方法,因為 a 尚未釋放鎖,b 只能等待。
- a 在獲取鎖的過程中會先讀 state,再寫 state。
- 當 a 釋放掉鎖並喚醒 b,b 會嘗試獲取鎖,也會先讀 state,再寫 state。
Happens-before 規則:一個 volatile 變量的寫操作發生在這個 volatile 變量隨后的讀操作之前。
- Java 自學指南
- Java 面試題匯總PC端瀏覽【點這里】
- Java知識圖譜
- Java 面試題匯總小程序瀏覽,掃二維碼
所有資源資源匯總於公眾號