1. 程式人生 > >深入理解Java虛擬機器(5)Java記憶體模型

深入理解Java虛擬機器(5)Java記憶體模型

深入理解Java虛擬機器(5)Java記憶體模型

Java記憶體模型

由於計算機的儲存與處理器的運算速度之間有幾個數量級的差距,會造成無法充分利用處理器,因此現代計算機加入了一層讀寫速度儘可能接近處理器運算速度的快取記憶體(cache)來作為記憶體與處理器之間的緩衝;先將運算需要的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步回記憶體之中,這樣處理器就不需要等待緩慢的記憶體讀寫。 對於現在的多核機器,有多個CPU,每個CPU有自己的快取記憶體,這就需要進行快取一致性,不然同步回記憶體時會出現衝突,這裡的一致性是靠一致性協議來完成的。如圖:
在這裡插入圖片描述

主記憶體和工作記憶體

在這裡插入圖片描述
如圖,主記憶體和工作記憶體,主記憶體就是儲存執行緒共享變數的地方,工作記憶體與執行緒對應,是執行緒私有的,速度快很多,是主記憶體中變數的拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,不同執行緒之間的變數值的傳遞均需要通過主記憶體來完成。

記憶體間的互動操作,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實現細節,主要通過以下八個指令來完成(這八個指令均是原子性的):lock、unlock、read、load、use、assign、store、write。

volatile關鍵字

volatile關鍵字有兩個語義。
語義1:定義為volatile的變數對所有執行緒可見,即一個執行緒中修改該值,別的執行緒可以立即得知。普通變數修改後,別的執行緒不會立即得知,需要先同步回主記憶體,別的執行緒再從主記憶體中取到這個變數才會拿到新值。

問題:volatile關鍵字是執行緒安全的嗎?
回答:不是,舉個例子,如下程式碼片段

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);
	}
}	

這段程式碼發起了20個執行緒,每個執行緒對race變數進行10000次自增操作,如果volatile關鍵字執行緒安全,則最終會輸出200000。但是實際執行後,最終的輸出會小於200000,因為volatile關鍵字雖然讓該變數對所有執行緒可見,但比如,目前執行緒1執行自增操作時,發現volatile修飾的變數值為10,然後準備在10的基礎上進行自增操作,但此時執行緒2將volatile關鍵字增加到了11,但執行緒1還是在10的基礎上進行自增,然後執行緒1將該值增加到11,並同步給了其他執行緒,這樣就使被volatile修飾的這個變數值變小了。
因此volatile關鍵字不是執行緒安全的。

語義2:禁止指令重排序優化,這可以防止併發中某些錯誤的發生。
比如,執行緒A要初始化配置資訊供執行緒B使用,初始化後將flag標誌位true,執行緒B發現flag為true時讀取配置資訊,如果不使用volatile關鍵字,執行緒A重排序可能會在沒有初始化資訊的情況下將flag設為true,此時執行緒B讀取配置資訊發現沒有,因此出錯。
用volatile關鍵字可以禁止指令重排序,原理是加上一個記憶體屏障,指令重排序無法把後面的指令重排序到記憶體屏障前面去。

long與double型別的特殊規則

對於long和double型別變數的特殊規則,因為long和double型別都是64位,規定允許虛擬機器將沒有被volatile關鍵字修飾的64位資料拆分成兩次32位的操作來進行,即允許虛擬機器可以選擇不保證64位資料型別的load、store、read、write這四個操作的原子性,這樣就不會是執行緒安全的,但是在大多數的虛擬機器中還是會選擇將這些操作實現為原子性的操作,所以日常編寫併發程式時不需要專門對long和double型別的資料進行特殊處理。

synchronized關鍵字

synchronized關鍵字表示一個變數一個時刻只允許一條執行緒對其進行lock操作。類似於作業系統中同一時刻只允許一個執行緒進入臨界區。
該關鍵字可以修飾變數,物件和類。
修飾變數時表示對該變數,一個時刻只允許一條執行緒對其進行lock操作。
修飾物件時表示對該物件,一個時刻只允許一條執行緒對其進行lock操作。
修飾類時表示對於該類的所有物件,靜態量等,一個時刻只允許一條執行緒對其進行lock操作。

另外在此順便提一下,保證有序性的還有先行發生原則(與時間發生順序基本無關)。