天天看點

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

1.原子性

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

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

i++ 不是原子操作

1.讀取i

2.對i進行加1操作

3.寫回i

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

在多線程的操作下,可能同時執行了讀操作,進而使它們得到相同的值,并都加1,結果就是多線程執行下結果是一樣的。執行了2次i++後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);
		}
	}
}
           
JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

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

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

② 譯碼和取寄存器操作數  ID    (将參數拿出來)

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

④ 存儲器通路  MEM            

⑤ 寫回  WB                    (寫回寄存器)

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

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

2.指令重排

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

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

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

第一條指令執行IF的時候第二條指令是不能執行的,因為第一條指令占用指令寄存器的時候,第二條不能使用同樣的硬體,但是當第一條指令執行到ID的時候,就空出了執行IF時占用的硬體,這樣第二條指令就能開始執行IF

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

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

例如:

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before
JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before
JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

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

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

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

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

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

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

3.可見性

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

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

編譯器優化:

一個編譯程式,在編譯代碼的時候,一個線程中的變量的值優化到了寄存器中,另外一個線程将這個變量放入了高速緩存cache中,這時候這2個線程未必能在同一時間發現對方修改了這個變量,畢竟在多核CPU每個CPU都有自己的一套寄存器,自己的一套cache,每個變量可能被不同線程的寄存器或cache緩存住,是以不能保準它們之間一定是一緻的。

JAVA高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

硬體優化

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

Java虛拟機層面的可見性

來源:

http://hushi55.github.io/2015/01/05/volatile-assembly

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高并發學習筆記(三) JMM(Java記憶體模型)1.原子性2.有序性3.可見性4.Happend-Before

java虛拟機優化後導緻這種現象。可以在聲明stop變量的時候加個volatile關鍵字避免這種問題

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

4.Happend-Before

程式順序原則:一個線程内保證語義的串行性

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

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

l n 傳遞性:A先于B,B先于C,那麼A必然先于C

l n 線程的start()方法先于它的每一個動作

l n 線程的所有操作先于線程的終結(Thread.join())

l n 線程的中斷(interrupt())先于被中斷線程的代碼

l n 對象的構造函數執行結束先于finalize()方法

繼續閱讀