JMM中的重排序及內存屏障

1. 概述

在執行程序時, 爲了提升性能, 編譯器和處理器經常會對指令作重排序. 爲了實現某些功能有時會禁止某些重排序, 由此引入了內存屏障.程序員

2. 重排序

重排序雖然能夠提升程序性能, 可是編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序. 即: 編譯器和處理器在重排序時, 會遵
守數據依賴性.緩存

這裏說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做, 不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮.多線程

2-1. as-if-serial語義

as-if-serial語義的意思是: 無論怎麼重排序(編譯器和處理器爲了提升並行度), (單線程)程序的執行結果不能被改變. 編譯器、runtime和處理器都必須遵照as-if-serial語義.性能

爲了遵照as-if-serial語義, 編譯器和處理器不會對存在數據依賴關係的操做作重排序, 由於這種重排序會改變執行結果. 可是, 若是操做之間不存在數據依賴關係, 這些操做就可能被編譯器和處理器重排序.優化

2-2. 重排序的種類

  1. 編譯器優化的重排序: 編譯器在不改變單線程程序語義的前提下, 能夠從新安排語句的執行順序.
  2. 指令級並行的重排序: 現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行. 若是不存在數據依賴性, 處理器能夠改變語句對應機器指令的執行順序.
  3. 內存系統的重排序: 因爲處理器使用緩存和讀/寫緩衝區, 這使得加載和存儲操做看上去多是在亂序執行.

2-3. 從Java源代碼到最終實際執行的指令序列, 會分別經歷下面3中重排序.

源代碼 -> 1:編譯器優化重排序 -> 2:指令級並行重排序 -> 3:內存系統重排序 -> 最終執行的指令序列線程

其中1屬於編譯器重排序, 2和3屬於處理器重排序. 這些重排序可能會致使多線程程序出現內存可見性問題. 對於編譯器, JMM編譯器重排序規則會禁止特性類型的編譯器重排序(並非全部的編譯器重排序都要禁止); 對於處理器重排序, JMM的處理器重排序規則會要求Java編譯器在生成指令序列時, 插入特性類型的內存屏障(Memory Barriers, Intel稱之爲Memory Fence)指令, 經過內存屏障指令來禁止特定類型的處理器重排序.3d

JMM屬於語言級的內存模型, 它確保在不一樣的編譯器和不一樣的處理器平臺之上, 經過禁止特性類型的編譯器重排序和處理器重排序, 爲程序員提供一致的內存可見性保證.blog

3. 內存屏障類型

現代的CPU使用寫緩衝區臨時保存向內存寫入的數據. 寫緩衝區能夠保證指令流水線持續運行, 它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延遲. 同時, 經過以批處理的方式刷新寫緩衝區, 以及合併寫緩衝區中對同一內存地址的屢次寫, 減小對內存總線的佔用. 雖然寫緩衝區有這麼多好處, 可是每一個處理器的寫緩衝區僅僅對它所在的處理器可見. 這個特性會對內存操做的執行順序產生重要的影響: 處理器對內存的讀/寫操做的執行順序. 不必定與內存實際發生的讀寫操做順序一致.排序

寫緩衝區僅對本身的處理器可見, 它會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致. 因爲處理器都會使用寫緩衝區, 所以現代處理器都會容許對寫-讀操做進行重排序.

3-1. 處理器的重排序規則

能夠發現常見的處理器都容許StoreLoad重排序; 常見的處理器都不容許對存在數據依賴的操做作重排序. SPARC-TSO和X86擁有相對較強的處理器內存模型, 它們僅容許對寫-讀操做作重排序(由於它們都使用了寫緩衝區).

3-2. 內存屏障類型表

爲了保證內存可見性, Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序.

StoreLoad Barriers是一個"全能型"的屏障, 它同時具備其餘3個屏障的效果. 現代的多處理器大多支持該屏障(其餘類型的屏障不必定被全部處理器支持). 執行該屏障開銷會很昂貴, 由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(Buffer Fully Flush).

4. 總結

重排序能夠提升性能, 可是重排序可能會致使內存可見性問題, 問了解決這個問題, 編譯器在生成字節碼的時候會插入特定類型的內存屏障來禁止重排序, 保證多線程下的內存可見性.

相關文章
相關標籤/搜索