1. 程式人生 > >JAVA高併發學習筆記(三) JMM(Java記憶體模型)

JAVA高併發學習筆記(三) JMM(Java記憶體模型)

1.原子性

原子性是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾。

一般的CPU的指令都是原子性的操作

i++ 不是原子操作

1.讀取i

2.對i進行加1操作

3.寫回i


在多執行緒的操作下,可能同時執行了讀操作,從而使它們得到相同的值,並都加1,結果就是多執行緒執行下結果是一樣的。執行了2i++i的值卻不是3

2.有序性

在併發時,程式的執行可能就會出現亂序

1.執行亂序

public class OrderExample {
	
	static int a = 0 ;
	static boolean flag = false;
	
	public static void writer() {
		a = 1;
		flag = true;
	}
	
	public static void reader() {
		if (flag) {
			int i = a + 1;
			System.out.println(i);
		}
	}
}

假設:一條指令的執行是可以分為以下步驟

① 取值  IF    (將指令取出來)

② 譯碼和取暫存器運算元  ID    (將引數拿出來)

③ 執行或者有效地址計算  EX    (執行)

④ 儲存器訪問  MEM            

⑤ 寫回  WB                    (寫回暫存器)

這個執行是按照順序來的 從上至下

每一步可能會使用不同的硬體

2.指令重排

一般我們認為的執行方式可能會是:


假設每個環節都會消耗一個CPU時鐘週期2條指令序列,會消耗10CPU時鐘週期,這樣太浪費時間了

第一條指令執行IF的時候第二條指令是不能執行的,因為第一條指令佔用指令暫存器的時候,第二條不能使用同樣的硬體,但是當第一條指令執行到

ID的時候,就空出了執行IF時佔用的硬體,這樣第二條指令就能開始執行IF

所以指令的執行方式應該是:


例如:




因為下面2LW操作和上面的操作沒有必然的聯絡,我們可以將指令進行重排


本來是14個時鐘週期執行完成的經過重排之後變成了12個時鐘週期

指令重排的原則不能破壞序列語義的執行

指令重排是優化程式碼的一種方式

優化的結果就是一個執行緒去看另外執行緒的執行順序可能會出現亂序的現象,破壞多執行緒的語義

3.可見性

可見性是指當一個執行緒修改了某一個共享變數的值,其他執行緒是否能夠立即知道這個修改

可能由各個環節優化產生,沒有辦法從一個執行緒當中看另外一個執行緒一個變數執行到什麼程度去推測另外一個變數的情況。

編譯器優化:

一個編譯程式,在編譯程式碼的時候,一個執行緒中的變數的值優化到了暫存器中,另外一個執行緒將這個變數放入了快取記憶體cache中,這時候這2個執行緒未必能在同一時間發現對方修改了這個變數,畢竟在多核CPU每個CPU都有自己的一套暫存器,自己的一套cache,每個變數可能被不同執行緒的暫存器或cache快取住,所以不能保準它們之間一定是一致的。


硬體優化

比如CPU想把資料寫入記憶體中去,可能並不是直接把資料寫入記憶體中,因為這樣會很慢。為了優化,會有一個硬體佇列,先把資料寫入硬體佇列,通過批操作,把資料批量寫入記憶體,如果對同一個記憶體地址做了多次不同的讀寫,認為這是不必要的,因為一定是最後一次讀寫為準,對前面幾次的記憶體地址讀寫就不操作了不放入佇列中,而對最後結果寫入記憶體。導致之前幾次對記憶體的讀寫其他執行緒是看不到的。

Java虛擬機器層面的可見性

來源:

public class VisibilityTest extends Thread {

	private boolean stop;
	
	@Override
	public void run() {
		int i = 0;
		while (!stop) {
			i++;
		}
		System.out.println("finish loop, i = " + i);
	}
	
	public void stopIt() {
		stop = true;
	}
	
	public boolean getStop() {
		return stop;
	}
	
	public static void main(String[] args) throws InterruptedException {
		
		VisibilityTest v = new VisibilityTest();
		v.start();
		
		Thread.sleep(1000);
		v.stopIt();
		Thread.sleep(2000);
		
		System.out.println("finish main");
		System.out.println(v.getStop());
		
	}
	
}

-server模式執行上述程式碼,永遠不會停止

執行執行緒的彙編程式碼:



java虛擬機器優化後導致這種現象。可以在宣告stop變數的時候加個volatile關鍵字避免這種問題

可見性問題的成因是比較複雜的,可能由各個層面上的優化產生的。可見性問題就是在一個執行緒中看不到另外一個執行緒對某個變數的修改


4.Happend-Before

程式順序原則:一個執行緒內保證語義的序列性

l n volatile規則volatile變數的寫,先發生於讀,這保證了volatile變數的可見性

l n 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前

l n 傳遞性A先於BB先於C,那麼A必然先於C

l n 執行緒的start()方法先於它的每一個動作

l n 執行緒的所有操作先於執行緒的終結(Thread.join()

l n 執行緒的中斷(interrupt())先於被中斷執行緒的程式碼

l n 物件的建構函式執行結束先於finalize()方法