1. 概述
在執行程序時, 為了提高性能, 編譯器和處理器常常會對指令做重排序. 為了實現某些功能有時會禁止某些重排序, 由此引入了內存屏障.
2. 重排序
重排序雖然可以提高程序性能, 但是編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序. 即: 編譯器和處理器在重排序時, 會遵
守數據依賴性.
這里說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作, 不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮.
2-1. as-if-serial語義
as-if-serial語義的意思是: 不管怎么重排序(編譯器和處理器為了提高並行度), (單線程)程序的執行結果不能被改變. 編譯器、runtime和處理器都必須遵守as-if-serial語義.
為了遵守as-if-serial語義, 編譯器和處理器不會對存在數據依賴關系的操作做重排序, 因為這種重排序會改變執行結果. 但是, 如果操作之間不存在數據依賴關系, 這些操作就可能被編譯器和處理器重排序.
2-2. 重排序的種類
- 編譯器優化的重排序: 編譯器在不改變單線程程序語義的前提下, 可以重新安排語句的執行順序.
- 指令級並行的重排序: 現代處理器采用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行. 如果不存在數據依賴性, 處理器可以改變語句對應機器指令的執行順序.
- 內存系統的重排序: 由於處理器使用緩存和讀/寫緩沖區, 這使得加載和存儲操作看上去可能是在亂序執行.
2-3. 從Java源代碼到最終實際執行的指令序列, 會分別經歷下面3中重排序.
源代碼 -> 1:編譯器優化重排序 -> 2:指令級並行重排序 -> 3:內存系統重排序 -> 最終執行的指令序列
其中1屬於編譯器重排序, 2和3屬於處理器重排序. 這些重排序可能會導致多線程程序出現內存可見性問題. 對於編譯器, JMM編譯器重排序規則會禁止特性類型的編譯器重排序(並不是所有的編譯器重排序都要禁止); 對於處理器重排序, JMM的處理器重排序規則會要求Java編譯器在生成指令序列時, 插入特性類型的內存屏障(Memory Barriers, Intel稱之為Memory Fence)指令, 通過內存屏障指令來禁止特定類型的處理器重排序.
JMM屬於語言級的內存模型, 它確保在不同的編譯器和不同的處理器平台之上, 通過禁止特性類型的編譯器重排序和處理器重排序, 為程序員提供一致的內存可見性保證.
3. 內存屏障類型
現代的CPU使用寫緩沖區臨時保存向內存寫入的數據. 寫緩沖區可以保證指令流水線持續運行, 它可以避免由於處理器停頓下來等待向內存寫入數據而產生的延遲. 同時, 通過以批處理的方式刷新寫緩沖區, 以及合並寫緩沖區中對同一內存地址的多次寫, 減少對內存總線的占用. 雖然寫緩沖區有這么多好處, 但是每個處理器的寫緩沖區僅僅對它所在的處理器可見. 這個特性會對內存操作的執行順序產生重要的影響: 處理器對內存的讀/寫操作的執行順序. 不一定與內存實際發生的讀寫操作順序一致.
寫緩沖區僅對自己的處理器可見, 它會導致處理器執行內存操作的順序可能會與內存實際的操作執行順序不一致. 由於處理器都會使用寫緩沖區, 因此現代處理器都會允許對寫-讀操作進行重排序.
3-1. 處理器的重排序規則
可以發現常見的處理器都允許StoreLoad重排序; 常見的處理器都不允許對存在數據依賴的操作做重排序. SPARC-TSO和X86擁有相對較強的處理器內存模型, 它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區).
3-2. 內存屏障類型表
為了保證內存可見性, Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序.
StoreLoad Barriers是一個"全能型"的屏障, 它同時具有其他3個屏障的效果. 現代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持). 執行該屏障開銷會很昂貴, 因為當前處理器通常要把寫緩沖區中的數據全部刷新到內存中(Buffer Fully Flush).
4. 總結
重排序可以提高性能, 但是重排序可能會導致內存可見性問題, 問了解決這個問題, 編譯器在生成字節碼的時候會插入特定類型的內存屏障來禁止重排序, 保證多線程下的內存可見性.