volatile關鍵字的作用是什么?


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 變量隨后的讀操作之前。


 


 

所有資源資源匯總於公眾號



 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM