深入Java併發編程(二):深入理解JMM

一、前言

內存和線程的關係就跟水和🐟一樣,沒有內存線程壓根跑不起來。而 Java內存模型是爲了解決不同平臺下的硬件和操作系統的內存模型差異而被定義的,以達到java的程序能夠在不同平臺下都能有一致的內存訪問的效果。

二、主內存和工作內存

JMM規定了內存主要劃分爲主內存和工作內存。主內存是線程公有的,所有的線程都可以對其進行讀寫;而工作內存是線程私有的,用來拷貝主內存中的變量。其中主內存中存放共享變量,主要對應的是Java堆中的對象實例,而工作內存對於棧中的部分區域,主要是一些方法參數以及局部變量。爲什麼要劃分出主內存和工作內存呢?其實更底層的去看,主內存對應的是硬件的物理內存,而工作內存對應的是高速緩存和寄存器。程序運行時主要訪問讀寫的就是我們的工作內存。

線程對主內存以及互相之間的工作內存的訪問讀寫都必須遵守以下規則:

1、線程在對變量的所有操作都必須在自己的工作內存中進行。因此在對變量操作前要先將變量拷貝到自己的工作內存中而不是直接讀寫主內存中的變量;

2、主內存是不同線程的工作內存之間數據交互的中轉站,也即是說,不同的工作線程之間的變量不能直接交換數據,而是要先寫入主內存,主內存再把數據寫入需要的工作內存。

三、內存數據交互操作

我們在(二)都知道了主內存和工作內存的變量是怎麼交互的,但它具體的實現細節還是不明瞭的。這裏介紹一下內存之間變量交互的8種原子操作,這對於我們後面學習理解synchronized、volatile等關鍵字有很大的幫助。

1、lock(鎖定):把主內存中的一個變量標識爲線程獨佔狀態

2、unlock(解鎖):釋放主內存中處於鎖定狀態的變量,讓它能被其他線程訪問讀寫

3、read(讀取):將主內存中的變量的值拷貝到線程的工作內存中

4、load(載入):緊接read操作,將主內存拷貝來的值放入工作內存的變量副本里

5、use(使用):把工作內存中的一個變量值傳給執行引擎

6、assign(賦值):把執行引擎的值賦給工作內存的一個變量

7、store(存儲):把工作內存中一個變量的值傳入主內存

8、write(寫入):緊接store,把傳入的工作內存的值賦給主內存的一個變量

看到read和load操作,我們很合理地可以推測出這兩個操作是需要連用的,它們單獨的出現是沒有意義的。而store和write也是如此。

四、JMM中的三個特性

1、原子性:read、load、use、assign、store、write這6個原子操作是直接的保證當前變量的原子性。而lock和unlock操作則在保證了一個更大範圍的原子性操作,比如基於unlock和lock實現的synchronized關鍵字,它可以保證整個同步塊的原子性。

2、可見性:可見性是針對於共享變量而言的,如果一個線程修改了共享變量的值,那麼其他線程都可以立即得知這個修改。我們通常用volatile來保證可見性,它可以將工作內存中修改的值立即同步到主內存中,不過這並不意味着它修飾的值在所有工作內存中都是一致的,只有當要使用這個變量前,我們纔會去讀取主內存中被修改過的變量並重新載入。事實上synchronized和final也可以保證可見性。synchronized的可見性是由"在執行unlock前必須把該變量同步回主內存"這條規則保證的,而final關鍵字的可見性是指final修飾的字段一旦初始化完成那麼其他線程也可以看到final字段的值。

3、有序性:單個線程內的所有操作都是有序的(程序次序規則保證了一個線程內的代碼按照控制流順序執行)。但如果是多個線程,那麼就可能回因爲指令重排序而導致操作的無序,這時候考慮使用synchronized或是volatile。synchronized可以確保“一個變量在同一時刻只允許一個線程對其進行lock操作”,這使得同步塊只能串行地執行,而volatile則是通過禁止指令重排序保證的。

 

五、happens-before原則

happens-before原則指的是java定義的兩項操作的偏序關係 。對於線程和線程間的常見操作,我們不可能一直用同步手段去保證它們的有序性,這會使得我們的操作非常的繁瑣,這時,我們就需要先行發生原則去規定線程和線程間的一些操作的先行發生關係。關於先行發生的關係,我們來舉個栗子:

a = 10;//線程A

b = a+10;//線程B

a = 11;//線程C

在此處我們假設線程A的"a=10"操作happens before 線程B,那麼B就能觀測到線程A操作的影響,在這裏也就是a修改後的值,所以b可以正確的被賦值爲20。倘若此時的線程C發生在線程A之後,但與B的先後關係不確定,那麼此時b的值就不確定了,因爲B可能觀察到線程C對a地影響,可能也觀察不到。

JMM默認的先行發生規則有以下幾條,這些規則無需同步手段去保障它們的順序性:

1、程序次序規則:在一個線程內,按照控制流順序,前面操作先行於後面操作 

2、管程鎖定規則:對於同一個鎖,它的unlock操作先行於lock操作

3、volatile變量規則:對於同一個volatile變量,它的寫操作先行於後面的讀操作

4、線程啓動規則:Thread對象地start()方法先行於它的任何操作

5、線程終止規則:線程的所有操作都先行於此線程的每一個動作

6、線程中斷規則:對線程interrupt(0的調用線性於被中斷線程的代碼檢測到中斷事件的發生

7、對象終結規則:一個對象的初始化完成先行於它的finalize()方法的開始

8、傳遞性:如果A操作先行於B操作,B操作先行於C操作,那麼A操作先行於C操作

六、總結

由於Java中併發的大多都是線程,所以定義工作內存作爲線程私有的內存空間是很有必要的,這能加快線程的I/O速度,減少CPU等待事件從而提高CPU利用率。而主內存和工作內存之間嚴謹的變量交換規則則儘可能地保證了數據的一致性。而synchronized這樣的擴大化原子操作既規定了線程間的串行化執行,又滿足了原子性。volatile、synchronized、final則是從不同的角度去滿足可見性原則。而happens-before原則則既是幫助確立了不依賴同步機制下線程本身和線程之間的操作偏序關係,也能作爲我們去快速判斷操作的偏序關係的重要標準。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章