Java記憶體模型與volatile關鍵字

Colin Wang發表於2015-06-11

Java記憶體模型(Java Memory Model)

Java記憶體模型(JMM),不同於Java執行時資料區,JMM的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體中讀取資料這樣的底層細節。JMM規定了所有的變數都儲存在主記憶體中,但每個執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,工作記憶體是執行緒之間獨立的,執行緒之間變數值的傳遞均需要通過主記憶體來完成。

Volatile關鍵字

平時在閱讀jdk原始碼的時候,經常看到原始碼中有寫變數被volatile關鍵字修飾,但是卻不是十分清除這個關鍵字到底有什麼用處,現在終於弄清楚了,那麼我就來講講這個volatile到底有什麼用吧。

當一個變數被定義為volatile之後,就可以保證此變數對所有執行緒的可見性,即當一個執行緒修改了此變數的值的時候,變數新的值對於其他執行緒來說是可以立即得知的。可以理解成:對volatile變數所有的寫操作都能立刻被其他執行緒得知。但是這並不代表基於volatile變數的運算在併發下是安全的,因為volatile只能保證記憶體可見性,卻沒有保證對變數操作的原子性。比如下面的程式碼:

/**
 * 發起20個執行緒,每個執行緒對race變數進行10000次自增操作,如果程式碼能夠正確併發,
 * 則最終race的結果應為200000,但實際的執行結果卻小於200000。
 * 
 * @author Colin Wang
 *
 */
public class VolatileTest {
	public static volatile int race = 0;

	public static void increase() {
		race++;
	}

	private static final int THREADS_COUNT = 20;

	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];

		for (int i = 0; i < THREADS_COUNT; i++) {
			threads[i] = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
						increase();
					}
				}
			});
			threads[i].start();
		}

		while (Thread.activeCount() > 1)
			Thread.yield();

		System.out.println(race);
	}
}

這便是因為race++操作不是一個原子操作,導致一些執行緒對變數race的修改丟失。若要使用volatale變數,一般要符合以下兩種場景:

  1. 變數的運算結果並不依賴於變數的當前值,或能夠保證只有單一的執行緒修改變數的值。
  2. 變數不需要與其他的狀態變數共同參與不變約束。

使用volatile變數還可以禁止JIT編譯器進行指令重排序優化,這裡使用單例模式來舉個例子:

/**
 * 單例模式例程一
 * 
 * @author Colin Wang
 *
 */
public class Singleton_1 {

	private static Singleton_1 instance = null;

	private Singleton_1() {
	}

	public static Singleton_1 getInstacne() {
		/*
		 * 這種實現進行了兩次instance==null的判斷,這便是單例模式的雙檢鎖。
		 * 第一次檢查是說如果物件例項已經被建立了,則直接返回,不需要再進入同步程式碼。
		 * 否則就開始同步執行緒,進入臨界區後,進行的第二次檢查是說:
		 * 如果被同步的執行緒有一個建立了物件例項, 其它的執行緒就不必再建立例項了。
		 */
		if (instance == null) {
			synchronized (Singleton_1.class) {
				if (instance == null) {
					/*
					 * 仍然存在的問題:下面這句程式碼並不是一個原子操作,JVM在執行這行程式碼時,會分解成如下的操作:
					 * 1.給instance分配記憶體,在棧中分配並初始化為null
					 * 2.呼叫Singleton_1的建構函式,生成物件例項,在堆中分配 
					 * 3.把instance指向在堆中分配的物件
					 * 由於指令重排序優化,執行順序可能會變成1,3,2,
					 * 那麼當一個執行緒執行完1,3之後,被另一個執行緒搶佔,
					 * 這時instance已經不是null了,就會直接返回。
					 * 然而2還沒有執行過,也就是說這個物件例項還沒有初始化過。
					 */
					instance = new Singleton_1();
				}
			}
		}
		return instance;
	}
}
/**
 * 單例模式例程二
 * 
 * @author Colin Wang
 *
 */
public class Singleton_2 {

	/*
	 * 為了避免JIT編譯器對程式碼的指令重排序優化,可以使用volatile關鍵字,
	 * 通過這個關鍵字還可以使該變數不會在多個執行緒中存在副本,
	 * 變數可以看作是直接從主記憶體中讀取,相當於實現了一個輕量級的鎖。
	 */
	private volatile static Singleton_2 instance = null;

	private Singleton_2() {
	}

	public static Singleton_2 getInstacne() {
		if (instance == null) {
			synchronized (Singleton_2.class) {
				if (instance == null) {
					instance = new Singleton_2();
				}
			}
		}
		return instance;
	}
}

變數在有了volatile修飾之後,對變數的修改會有一個記憶體屏障的保護,使得後面的指令不能被重排序到記憶體屏障之前的位置。volalite變數的讀效能與普通變數類似,但是寫效能要低一些,因為它需要插入記憶體屏障指令來保證處理器不會發生亂序執行。即便如此,大多數場景下volatile的總開銷仍然要比鎖低,所以volatile的語義能滿足需求時候,選擇volatile要優於使用鎖。

相關文章