1. 程式人生 > >Java基礎: 什麼是指令重排序/as-if-serial/記憶體屏障/happens-before

Java基礎: 什麼是指令重排序/as-if-serial/記憶體屏障/happens-before

Java基礎知識

指令重排序

在執行程式時,為了提高效能,編譯器和處理器會對指令做重排序。

  1. 編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級並行的重排序:如果不存l在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序:處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

但是,可以通過插入特定型別的Memory Barrier來禁止特定型別的編譯器重排序和處理器重排序。

資料依賴性

如果兩個操作訪問同一個變數,其中一個為寫操作,此時這兩個操作之間存在資料依賴性。 
編譯器和處理器不會改變存在資料依賴性關係的兩個操作的執行順序,即不會重排序。

as-if-serial

不管怎麼重排序,單執行緒下的執行結果不能被改變。

編譯器、runtime和處理器都必須遵守as-if-serial語義。

記憶體屏障(Memory Barrier )

上面講到了,通過記憶體屏障可以禁止特定型別處理器的重排序,從而讓程式按我們預想的流程去執行。

記憶體屏障,又稱記憶體柵欄,是一個CPU指令,基本上它是一條這樣的指令:

  1. 保證特定操作的執行順序。
  2. 影響某些資料(或則是某條指令的執行結果)的記憶體可見性。

編譯器和CPU能夠重排序指令,保證最終相同的結果,嘗試優化效能。插入一條Memory Barrier會告訴編譯器和CPU:不管什麼指令都不能和這條Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的資料,因此,任何CPU上的執行緒都能讀取到這些資料的最新版本。

java記憶體模型volatile是基於Memory Barrier實現的。

如果一個變數是volatile修飾的,JMM會在寫入這個欄位之後插進一個Write-Barrier指令,並在讀這個欄位之前插入一個Read-Barrier指令。這意味著,如果寫入一個volatile變數,就可以保證:

  1. 一個執行緒寫入變數a後,任何執行緒訪問該變數都會拿到最新值。
  2. 在寫入變數a之前的寫入操作,其更新的資料對於其他執行緒也是可見的。因為Memory Barrier會刷出cache中的所有先前的寫入。

Happens-Before

在JMM中,如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係,這個的兩個操作既可以在同一個執行緒,也可以在不同的兩個執行緒中。

我們需要關注的happens-before規則如下: (發生在...之前)

  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。

注意:兩個操作之間具有happens-before關係,並不意味前一個操作必須要在後一個操作之前執行!僅僅要求前一個操作的執行結果,對於後一個操作是可見的,且前一個操作按順序排在後一個操作之前。