valatile原理

一 volatile定義

官方定義:

java語言規範第三版中對volatile的定義如下:java編程語言允許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保通過排它鎖單獨獲得這個變量。java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明或volatile,java線程內存模型確保所有線程看到這個變量值是一致的。

個人定義:

volatile的解釋通常是“易變的,不穩定的”。這也正是使用volatile關鍵字的語義。當你用volatile聲明一個變量時,就等於告訴了虛擬機,這個變量極有可能被某些程序或者線程修改。爲了確保這個變量被修改後,應用程序範圍內所有線程都能夠“看到”這個改動,虛擬機就必須採用某些特殊手段,保證這個變量可見性等特點。

二 volatile有兩個性質,內存可見性和禁止指令重排序

內存可見性:

內存可見性表現在只要用該關鍵字修飾的域,當這個域內數據發生變化時,所有讀操作都可以看到這個修改,哪怕是使用了本地緩存,volatile域也會寫入到主存中,而讀寫操作正是發生在主存中。(主存又叫主存儲器 Main memory ,是計算機硬件的一個重要部件,其作用是存放指令和數據,並能由中央處理器(CPU)直接隨機存取)

指令重排序

在JDK中,JAVA語言爲了維持順序內部的順序化語義,也就是爲了保證程序的最終運行結果需要和在單線程嚴格意義的順序化環境下執行的結果一致,程序指令的執行順序有可能和代碼的順序不一致,這個過程就稱之爲指令的重排序。指令重排序的意義在於:JVM能根據處理器的特性,充分利用多級緩存,多核等進行適當的指令重排序,使程序在保證業務運行的同時,充分利用CPU的執行特點,最大的發揮機器的性能!

三 內存屏障與volatile的關係

內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令:
a) 確保一些特定操作執行的順序;
b) 影響一些數據的可見性(可能是某些指令執行後的結果)。
編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。內存屏障另一個作用是強制 更新一次不同CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。

四 volatile的實現原理

  1. 那麼Volatile是如何來保證可見性的呢?在x86處理器下通過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操作CPU會做什麼事情。

Java代碼: instance = new Singleton();//instance是volatile變量
彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

將當前處理器緩存行的數據會寫回到系統內存。有volatile變量修飾的共享變量進行寫操作的時候會多第二行彙編代碼,通過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引發了兩件事情。

  • 將當前處理器緩存行的數據會寫回到系統內存。
  • 這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效。

處理器爲了提高處理速度,不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)後再進行操作,但操作完之後不知道何時會寫到內存,如果對聲明瞭Volatile變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏。
擴展一下:L1,L2爲cpu的一級緩存和二級緩存,相應的L3爲三級緩存,它們的容量逐漸遞增,成本逐漸遞減。L2相當於L1的緩衝器,存儲L1需要用到而又無法存儲的數據。L3爲L2的緩衝器。需要注意,L2和L3和內存都不能存儲原始指令,只能存儲臨時數據。指令只能存在cpu的一級緩存中。

五 Happen-Before規則

雖然Java虛擬機和執行系統會對指令進行一定的重排,但是指令重排是有原則的,並非所有指令都可以隨隨便便改變執行位置,以下羅列了一些基本規則,這些原則是指令重排不可違背的。

  • 程序順序原則:一個線程內保證語義的串行性
  • volatile原則:volatile變量的寫,先發生於讀,這保證了volatile變量的可見性
  • 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
  • 傳遞性:A先於B,B先於C,那麼A必然先於C
  • 線程的start()方法先於它的每一個動作
  • 線程的所有操作先於線程的終結(Thread.join())
  • 線程的中斷(interrupt())先於被中斷線程的代碼
  • 對象的構造函數執行、結束先於finalize()方法

六 volatile的應用場景

synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些情況下性能要優於synchronized,但是要注意volatile關鍵字是無法替代synchronized關鍵字的,因爲volatile關鍵字無法保證操作的原子性。
通常來說,使用volatile必須具備以下2個條件:

  • 對變量的寫操作不依賴於當前值
  • 該變量沒有包含在具有其他變量的不變式中
    實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。
    事實上,我的理解就是上面的2個條件需要保證操作是原子性操作,才能保證使用volatile關鍵字的程序在併發時能夠正確執行。
    下面列舉個Java中使用volatile的幾個場景:
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;
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章