JVM | Java記憶體模型

來源:互聯網
上載者:User

標籤:緩衝   存在   重要   變化   線程   傳遞   作業系統   new   管理者   

前言

“天下武功,唯快不破”,火雲邪神告訴了你體術中追求的境界;相對論也告訴大家當你的移動速度逐漸超過光速甚至再快更快,你就很容易去到詩和遠方,遊火星,逛土星,浪跡天涯;當單核電腦從出現到一代代地提升效能,運算力也在更快更強。甚至就是奧運會都追求“更快、更高、更強”,似乎“快”對人們有著與生俱來的誘惑。那麼“快節奏和從前慢一生只夠愛一個人”,你又有著怎樣的思考呢,抱歉~這裡暫不討論。其實啊,人們不斷壓榨計算工具的運算力和老闆不停壓榨員工的體力一樣也都是有快感的。既然是壓榨,總有一天可能幾近榨不出油水,咋辦?

這不,單核CPU的主頻不可能無限制的增長,Intel老闆給跪了。再想提升效能,於是CPU進入多核時代,多個處理器協同工作。什麼,協同工作?小學自習課最能咋呼的是你,中學最不服管的也是你,其實很多時候不是追求的一加一大於二,而是一加一不小於一,人越多越亂,事越多越煩,一個道理,增加CPU數量可不是簡單的一加一,變數越多,帶來的不確定性也就越多,天賦異稟的人更適合做管理者,當然具備足夠完善和周密的演算法的作業系統才能協同好電腦。

多任務處理在現代電腦作業系統中幾乎是一項必備的功能。許多情況下,讓電腦同時去做幾件事情,不僅是因為電腦的運算能力強大了,還有一個很重要的原因是電腦的運算速度與它的儲存和通訊子系統速度的差距太大。電腦的存放裝置與處理器的運算速度有幾個數量級的差距,所以現代電腦都不得不加入一層讀寫速度儘可能接近處理器運算速度的快取來作為記憶體與處理器之間的緩衝,這種解決思想呢就是緩衝技術。
基於快取的儲存互動很好地解決了處理器與記憶體的速度矛盾,但是也為電腦系統帶來了更高的複雜度,引入了新的問題:緩衝一致性。在多處理器系統中,每個處理器都有自己的快取,而又共用同一主記憶體。

處理器記憶體概念性模型

Java記憶體概念性模型

編譯器和處理器
  • 有著相同目標,在不改變程式執行結果的前提下,儘可能提高並行度。

  • 處理器不會改變存在資料依賴關係的兩個操作的執行順序。

  • 處理器保證單線程程式的重排序不改變執行結果。

  • 越是追求效能的處理器,記憶體模型設計得越弱,束縛少,儘可能多的最佳化來提高效能。

  • 編譯器不會改變存在資料依賴關係的兩個操作的執行順序。

  • 編譯器保證單線程程式的重排序不改變執行結果。
重排序
  • 處理器重排序

除了增加快取之外,為了使得處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執行最佳化,處理器會在計算之後將亂序執行的結構重組。

  • 資料依賴性

如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,那麼這兩個操作之間存在資料依賴性。
1)寫這個變數後,再讀這個變數;
2)寫這個變數後,再寫這個變數;
3)讀這個變數後,再寫這個變數;
上面3種情況,只要重排兩個操作的執行順序,執行結果就會被改變。

  • as-if-serial

語義是:不管編譯器和處理器為了提高並行度怎麼重排序,單線程程式的執行結果不能被改變。

  • happens-before

happens-before要求禁止的重排序分為兩類:
1)會改變程式執行結果的重排序。
JMM處理策略要求編譯器和處理器必須禁止這種重排序。
2)不會改變程式執行結果的重排序。
JMM處理策略允許編譯器和處理器這種重排序。

規則定義:
1)程式順序規則:一個線程中的每個操作,happens-before該線程的任意後續操作。
2)監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
5)start()規則:如果線程A執行操作ThreadB.start()啟動線程B,那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作。
6)join()規則:如果線程A執行操作Thread.join()並成功返回,那麼線程B中的任意操作happens-before於線程A從Thread.join()操作成功返回。

  • 小結

1)as-if-serial語義保證單線程內程式的執行結果不被改變;happens-before關係保證正確同步的多線程程式的執行結果不被改變。
2)as-if-serial語義創造單線程程式幻境:單線程程式是按程式的順序來執行的;happens-before關係創造多線程程式幻境:正確同步的多線程程式是按happens-before關指定的順序來執行的。

Java記憶體模型
  • Java記憶體模型規範對資料競爭的定義:
    在一個線程中寫一個變數,在另一個線程讀同一個變數,而且寫和讀沒有通過同步來排序。

  • JMM允許編譯器和處理器只要不改變程式執行結果,包括單線程程式和正確同步的多線程程式,怎麼最佳化都行。

  • 常見的處理器記憶體模型比JMM要弱,Java編譯器在產生位元組碼時,會在執行指令序列的適當位置插入記憶體屏障來限制處理器的重排序。各種處理器記憶體模型的強弱不同,JMM在不同處理器中插入的記憶體屏障的數量和種類也不相同。

  • 記憶體間互動操作:

1)lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條線程獨佔的狀態。
2)unlock(解鎖):作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,解鎖後的變數才可以被其他線程鎖定。
3)read(讀取):作用於主記憶體的變數,把一個變數的值從主記憶體傳輸到線程的工作記憶體中,以便隨後的load操作使用。
4)load(載入):作用於工作記憶體的變數,把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
5)use(使用):作用於工作記憶體的變數,把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到的變數的值的位元組碼指令時將會執行這個操作。
6)assign(賦值):作用於工作記憶體的變數,把一個從執行引擎收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
7)store(儲存):作用於工作記憶體的變數,把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
8)write(寫入):作用於主記憶體的變數,把store操作沖工作記憶體中得到的變數的值放入主記憶體的變數中。

  • 執行上述8種基本操作時必須滿足的規則:

1)不允許read和load、store和write操作之一單獨出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接受,或者從工作記憶體發起回寫了但主記憶體不接受的情況出現。
2)不允許一個線程丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。
3)不允許一個線程無原因地(沒有發生過任何assign操作)把資料從線程的工作記憶體同步回主記憶體中。
4)一個新的變數只能在主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化的變數,換句話說,對一個變數實施use、store操作之前,必須先執行過了assign和load操作。
5)一個變數在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。
6)如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始設定變數的值。
7)如果一個變數事先沒有被lock伺服器用戶端檔案鎖,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變數。
8)對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中。

鎖的記憶體語義

語義:

  • 當線程釋放鎖時,JMM會把該線程對應的本地記憶體中的共用變數重新整理到主記憶體中。
  • 當線程擷取鎖時,JMM會把該線程對應的本地記憶體置為無效。

線程A釋放一個鎖,實質上是線程A向接下來將要擷取這個鎖的某個線程發出了(線程A對共用變數所做修改的)訊息。
線程B擷取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共用變數所做修改的)訊息。
線程A釋放鎖,隨後線程B擷取這個鎖,這個過程實質上是線程A通過主記憶體向線程B發送訊息。

實現:
見AQS等。

volatile記憶體語義

語義:

  • 當寫一個volatile變數時,JMM會把該線程對應的本地記憶體中的共用變數值重新整理到主記憶體。
  • 當讀一個volatile變數是,JMM會把該線程對應的本地記憶體置為無效。

線程A寫一個volatile變數,實質上是線程A向接下來將要讀這個volatile變數的某個線程發出了(其對共用變數所做修改的)訊息。
線程B讀一個volatile變數,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變數之前對共用變數所做修改的)訊息。
線程A寫一個volatile變數,隨後線程B讀這個volatile變數,這個過程實質上是線程A通過主記憶體向線程B發送訊息。

JMM實現:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。
  • 在每個volatile讀操作的前面插入一個LoadLoad屏障。
  • 在每個volatile讀操作的後面插入一個LoadStore屏障。

表現:

  • 可見度:對一個volatile變數的讀,總是能看到任意線程對這個volatile變數最後的寫入。
  • 原子性:對任意單個volatile變數的讀寫具有原子性,但類似volatile++這種符合操作不具備原子性。
final記憶體語義

對於final域,編譯器和處理器要遵守兩個重定序:

  • 在建構函式內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序。
  • 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。

編譯器final語義具體實現:

  • 寫final域的重定序會要求編譯器在final域的寫之後,建構函式return之前插入一個StoreStore屏障。
  • 讀final域的重定序要求編譯器在讀final域的操作前面插入一個LoadLoad屏障。
public class FinalExample {    int i;    final int j;    static FinalExample obj;    public FinalExample(){        i = 1;        j = 2;        // final域 StoreStore屏障 在這裡        // 確保建構函式return前 final域 j=2 完成    }    public static void write(){        obj = new FinalExample();    }    public static void reader(){        FinalExample object = obj;        int a = object.i;        // final域 LoadLoad屏障 在這裡        // 確保初次讀對象包含的final域前 讀對象引用完成        int b = object.j;    }}
總結
  • 處理器和編譯器都期望在不改變程式執行結果的前提下,儘可能提高並行度。
  • 處理器和編譯器都可能會對輸入代碼進行亂序執行最佳化,充分利用運算單元。
  • 處理器和編譯器都不會改變存在資料依賴關係的兩個操作的執行順序。
  • 處理器和編譯器都能保證單線程程式的重排序不改變執行結果。
  • 處理器記憶體模型都比JMM要弱,更偏向效能考慮。
  • JMM屏蔽了跨平台處理器,對不同處理器進行不同程度的禁止指令重排序,來儘可能保障正確語義。
  • 看透as-if-serial語義和happens-before關係,能協助程式員深入理解並發編程,編輯高效、健壯代碼。

參考文獻《深入理解JAVA 虛擬機器》、《Java並發編程的藝術》

JVM | Java記憶體模型

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.