java內存模型詳解

內存模型 (memory model)
內存模型描述的是程序中各變量(實例域、靜態域和數組元素)之間的關係,以及在實際計算機系統中將變量存儲到內存和從內存取出變量這樣的低層細節.

不同平臺間的處理器架構將直接影響內存模型的結構.

在C或C++中, 可以利用不同操作平臺下的內存模型來編寫併發程序. 但是, 這帶給開發人員的是, 更高的學習成本.
相比之下, java利用了自身虛擬機的優勢, 使內存模型不束縛於具體的處理器架構, 真正實現了跨平臺.
(針對hotspot jvm, jrockit等不同的jvm, 內存模型也會不相同)

內存模型的特徵:
a, Visibility 可視性 (多核,多線程間數據的共享)
b, Ordering 有序性 (對內存進行的操作應該是有序的)

java 內存模型 ( java memory model )

根據Java Language Specification中的說明, jvm系統中存在一個主內存(Main Memory或Java Heap Memory),Java中所有變量都儲存在主存中,對於所有線程都是共享的。

每條線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內存中進行,線程之間無法相互直接訪問,變量傳遞均需要通過主存完成。


其中, 工作內存裏的變量, 在多核處理器下, 將大部分儲存於處理器高速緩存中, 高速緩存在不經過內存時, 也是不可見的.

jmm怎麼體現 可視性(Visibility) ?
在jmm中, 通過併發線程修改變量值, 必須將線程變量同步回主存後, 其他線程才能訪問到.

jmm怎麼體現 有序性(Ordering) ?

通過java提供的同步機制或volatile關鍵字, 來保證內存的訪問順序.

緩存一致性(cache coherency)


什麼是緩存一致性?
它是一種管理多處理器系統的高速緩存區結構,其可以保證數據在高速緩存區到內存的傳輸中不會丟失或重複。(來自wikipedia)

舉例理解:
假如有一個處理器有一個更新了的變量值位於其緩存中,但還沒有被寫入主內存,這樣別的處理器就可能會看不到這個更新的值.

解決緩存一致性的方法?
a, 順序一致性模型:
要求某處理器對所改變的變量值立即進行傳播, 並確保該值被所有處理器接受後, 才能繼續執行其他指令.

b, 釋放一致性模型: (類似jmm cache coherency)
允許處理器將改變的變量值延遲到釋放鎖時才進行傳播.

jmm緩存一致性模型 – “happens-before ordering(先行發生排序)”

一般情況下的示例程序:

x = 0;
y = 0;
i = 0;
j = 0;

// thread A
y = 1;
x = 1;

// thread B
i = x;
j = y;

在如上程序中, 如果線程A,B在無保障情況下運行, 那麼i,j各會是什麼值呢?

答案是, 不確定. (00,01,10,11都有可能出現)
這裏沒有使用java同步機制, 所以 jmm 有序性和可視性 都無法得到保障.

happens-before ordering( 先行發生排序) 如何避免這種情況?
排序原則已經做到:
a, 在程序順序中, 線程中的每一個操作, 發生在當前操作後面將要出現的每一個操作之前.
b, 對象監視器的解鎖發生在等待獲取對象鎖的線程之前.
c, 對volitile關鍵字修飾的變量寫入操作, 發生在對該變量的讀取之前.
d, 對一個線程的 Thread.start() 調用 發生在啓動的線程中的所有操作之前.
e, 線程中的所有操作 發生在從這個線程的 Thread.join()成功返回的所有其他線程之前.

爲了實現 happends-before ordering原則, java及jdk提供的工具:
a, synchronized關鍵字
b, volatile關鍵字
c, final變量
d, java.util.concurrent.locks包(since jdk 1.5)
e, java.util.concurrent.atmoic包(since jdk 1.5)


使用了happens-before ordering的例子:



(1) 獲取對象監視器的鎖(lock)

(2) 清空工作內存數據, 從主存複製變量到當前工作內存, 即同步數據 (read and load)

(3) 執行代碼,改變共享變量值 (use and assign)

(4) 將工作內存數據刷回主存 (store and write)

(5) 釋放對象監視器的鎖 (unlock)

注意: 其中4,5兩步是同時進行的.

這邊最核心的就是第二步, 他同步了主內存,即前一個線程對變量改動的結果,可以被當前線程獲知!(利用了happens-before ordering原則)

對比之前的例子
如果多個線程同時執行一段未經鎖保護的代碼段,很有可能某條線程已經改動了變量的值,但是其他線程卻無法看到這個改動,依然在舊的變量值上進行運算,最終導致不可預料的運算結果。

經典j2ee設計模式Double-Checked Locking失效問題
雙重檢查鎖定失效問題,一直是JMM無法避免的缺陷之一.瞭解DCL失效問題, 可以幫助我們深入JMM運行原理.

要展示DCL失效問題, 首先要理解一個重要概念- 延遲加載(lazy loading).

非單例的單線程延遲加載示例:

Java代碼
  1. class  Foo  
  2. {  
  3. private  Resource res =  null ;  
  4. public  Resource getResource()  
  5. {  
  6.     // 普通的延遲加載   
  7. if  (res ==  null )  
  8.         res = new  Resource();  
  9. return  res;  
  10. }  
  11. }  
class Foo
{
private Resource res = null;
public Resource getResource()
{
    // 普通的延遲加載
if (res == null)
        res = new Resource();
return res;
}
}


非單例的 多線程延遲加載示例:

Java代碼
  1. Class Foo  
  2. {  
  3. Private Resource res = null ;  
  4. Public synchronized  Resource getResource()  
  5. {  
  6.       // 獲取實例操作使用同步方式, 性能不高   
  7. If (res == null ) res =  new  Resource();  
  8. return  res;  
  9. }  
  10. }  
Class Foo
{
Private Resource res = null;
Public synchronized Resource getResource()
{
      // 獲取實例操作使用同步方式, 性能不高
If (res == null) res = new Resource();
return res;
}
}



非單例的 DCL多線程延遲加載示例:

Java代碼
  1. Class Foo  
  2. {  
  3. Private Resource res = null ;  
  4. Public Resource getResource()  
  5. {  
  6. If (res == null )  
  7. {  
  8.        //只有在第一次初始化時,才使用同步方式.   
  9. synchronized ( this )  
  10. {  
  11. if (res ==  null )  
  12. {  
  13. res = new  Resource();  
  14. }  
  15. }  
  16. }  
  17. return  res;  
  18. }  
  19. }  
Class Foo
{
Private Resource res = null;
Public Resource getResource()
{
If (res == null)
{
       //只有在第一次初始化時,才使用同步方式.
synchronized(this)
{
if(res == null)
{
res = new Resource();
}
}
}
return res;
}
}



Double-Checked Locking看起來是非常完美的。但是很遺憾,根據Java的語言規範,上面的代碼是不可靠的。

出現上述問題, 最重要的2個原因如下:
1, 編譯器優化了程序指令, 以加快cpu處理速度.
2, 多核cpu動態調整指令順序, 以加快並行運算能力.

問題出現的順序:
1, 線程A, 發現對象未實例化, 準備開始實例化
2, 由於編譯器優化了程序指令, 允許對象在構造函數未調用完前, 將 共享變量的引用指向 部分構造的對象, 雖然對象未完全實例化, 但已經不爲null了.
3, 線程B, 發現部分構造的對象已不是null, 則直接返回了該對象.

不過, 一些著名的開源框架, 包括jive,lenya等也都在使用DCL模式, 且未見一些極端異常.
說明, DCL失效問題的出現率還是比較低的.
接下來就是性能與穩定之間的選擇了?

DCL的替代 Initialize-On-Demand :

Java代碼
  1. public   class  Foo {  
  2.     // 似有靜態內部類, 只有當有引用時, 該類纔會被裝載   
  3.     private   static   class  LazyFoo {  
  4.        public   static  Foo foo =  new  Foo();  
  5.     }  
  6.    
  7.     public   static  Foo getInstance() {  
  8.        return  LazyFoo.foo;  
  9.     }  
  10. }  
public class Foo {
    // 似有靜態內部類, 只有當有引用時, 該類纔會被裝載
    private static class LazyFoo {
       public static Foo foo = new Foo();
    }
 
    public static Foo getInstance() {
       return LazyFoo.foo;
    }
}



維基百科的DCL解釋:
http://en.wikipedia.org/wiki/Double-checked_locking

DCL的完美解決方案:
http://www.theserverside.com/patterns/thread.tss?thread_id=39606

總結:
多線程編程, 針對有寫操作的變量, 必須 保證其所有引用點與主存中數據一致(考慮採用同步或volatile) .

發佈了5 篇原創文章 · 獲贊 4 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章