Java內存模型JMM與可見性問題

在講述Java內存模型之前,先介紹幾個概念:指令重排序,Java運行模式

1.指令重排序

java編程語言的語義允許編譯器和處理器執行指令重排序優化,以提高程序運行效率。指令重排序需遵守as-if-serial,as-if-serial指的是:不管怎麼重排序,單線程程序的執行結果不能被改變。編譯器和處理器都必須遵守as-if-serial語義。也就是說,編譯器和處理器不會對存在數據依賴關係的操作做重排序。

指令重排序分爲:重排序和等效替換

 

2.java運行模式

  • 編譯:字節碼----jit提前編譯----彙編
  • 解釋:字節碼----jit一段一段編譯----彙編
  • 混合:綜合上述兩種,運行的過程中,jit編譯器生效,針對熱點代碼進行重排序優化並預編譯。可使用jitwatch查看優化後的彙編代碼

 

java內存模型

java內存模型描述程序可能的行爲,通過檢查執行跟蹤的每個讀操作,並根據某些規則檢查該讀操作觀察到的寫操作是否有效來工作。程序執行產生的所有結果都可以由內存模型預測。

具體來說,Java內存模型主要目標是定義程序中各個變量的訪問規則。所有的變量都是存在主存中,每個線程都有自己的工作內存。線程對變量的操作都必須在工作內存進行,不能直接對主存進行操作,並且每個線程不能訪問其他線程的工作內存。 

Java內存模型對於多線程三大特性的規定:

  • 原子性:java內存模型規定基本數據類型的訪問讀寫具備原子性
  • 可見性:通過一套同步規則,將變量修改後的新值同步回主內存,在變量讀取前從主內存刷新變量值到工作內存,實現可見性
  • 有序性:本線程內觀察到的所有操作都是有序的,但是在另一個線程中觀察該線程,由於重排序和同步延時,所有操作都是無序的。

Java中可見性存在CPU緩存等導致短期內可見性問題,重排序可能導致永久可見性問題。

可見性同步規則:

  1. 對於監視器m的解鎖,與所有後續操作對於m的加鎖同步
  2. 對volatile變量v的寫入,與所有後續對v的讀同步
  3. 啓動線程的操作,與線程中的第一個操作同步
  4. 對於每個屬性寫入默認值(0、false、null),與每個線程對其進行的操作同步
  5. 線程t1的最後操作,與線程t2發現t1已經結束同步(isAlive、join)。
  6. 如果線程t1中斷了t2,那麼線程t1的中斷操作與所有線程發現t2被中斷同步(InterruptedException,interrupted,isInterrupted)

Happens-before先行發生原則

happens-before關係主要用於強調兩個有衝突的動作之間的順序。對於共享變量的多次訪問,如果至少有一次是寫,那麼對於該變量訪問是衝突的。當程序包含兩個沒有被happens-before關係排序的衝突訪問時,就存在數據競爭。遵守這些原則,也就意味着有些代碼不能進行重排序,有些數據不能緩存。

虛擬機實現時,必須要確保以下原則(happens-before可以理解爲先發生對後發生可見)

  1. 某個線程中的每個動作都happens-before該線程中該動作之後的操作
  2. 某個monitor上的unlock動作happens-before同一monitor上的後續的lock操作
  3. 對某個volatile字段的寫操作happens-before每個後續對該volatile字段的讀操作
  4. 在某個線程對象上調用start()方法happens-before該啓動了的線程中的任意動作
  5. 某個線程中的所有動作happens-before任意其他線程成功從該線程對象上的join()中返回
  6. 具有傳遞性,如果某個動作a happens-before b,且b happens-before c,那麼a happens-before c

volatile關鍵字

volatile關鍵字可以讓一個線程對共享變量的修改,能夠及時被其他線程看到,遵循Java內存模型同步規則。

  • 禁止緩存:volatile變量的訪問控制符會加ACC_VOLATILE,保證立即可見
  • 禁止重排序:對volatile變量相關的指令不做重排序

volatile不保證原子性

final在JMM中的處理

  • 在共享對象的構造函數中設置的final字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。示例:f=new FinalDemo();若x爲final字段,則讀取到的f.x一定是最新的
  • 在共享對象構造函數中設置字段後發生讀取,如果該字段爲final,會看到字段分配的值,否則會看到默認值。示例:public FinalDemo(){x=1;y=x},y會等於1
  • 讀取共享對象的final字段時,要先讀取共享對象。示例:f= new FinalDemo();a=f.x;這兩個操作不能重排序
  • 通常final static是不可修改的字段,然而System.in、System.out、System.err是final static字段,由於遺留原因,必須允許通過set方法改變,我們見這些字段稱爲寫保護,以區別於普通final字段

double和long的特殊處理

虛擬機規範中,寫64位的double和long分成了兩次32位的操作,由於不是原子操作,可能導致讀取到的某次寫操作中的前32位,以及另一次寫操作的後32位。讀寫volatile的double和long總是原子的,引用的讀寫也總是原子的

雖然規範沒要求實現原子性,但是商業JVM大部分實現了原子性。

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