JMM Cookbook(一)指令重排
指令重排
對於編譯器的編寫者來說,Java記憶體模型(JMM)主要是由禁止指令重排的規則所組成的,其中包括了欄位(包括陣列中的元素)的存取指令和監視器(鎖)的控制指令。
Volatile與監視器
JMM中關於volatile和監視器主要的規則可以被看作一個矩陣。這個矩陣的單元格表示在一些特定的後續關聯指令的情況下,指令不能被重排。下面的表格並不是JMM規範包含的,而是一個用來觀察JMM模型對編譯器和執行系統造成的主要影響的工具。
能否重排 | 第二個操作 | ||
第一個操作 | Normal Load Normal Store |
Volatile load MonitorEnter | Volatile store MonitorExit |
Normal Load Normal Store |
No | ||
Volatile load MonitorEnter |
No | No | No |
Volatile store MonitorExit |
No | No |
關於上面這個表格一些術語的說明:
- Normal Load指令包括:對非volatile欄位的讀取,getfield,getstatic和array load;
- Normal Store指令包括:對非volatile欄位的儲存,putfield,putstatic和array store;
- Volatile load指令包括:對多執行緒環境的volatile變數的讀取,getfield,getstatic;
- Volatile store指令包括:對多執行緒環境的volatile變數的儲存,putfield,putstatic;
- MonitorEnters指令(包括進入同步塊synchronized方法)是用於多執行緒環境的鎖物件;
- MonitorExits指令(包括離開同步塊synchronized方法)是用於多執行緒環境的鎖物件。
在JMM中,Normal Load指令與Normal store指令的規則是一致的,類似的還有Volatile load指令與MonitorEnter指令,以及Volatile store指令與MonitorExit指令,因此這幾對指令的單元格在上面表格裡都合併在了一起(但是在後面部分的表格中,會在有需要的時候展開)。在這個小節中,我們僅僅考慮那些被當作原子單元的可讀可寫的變數,也就是說那些沒有位域(bit fields),非對齊訪問(unaligned accesses)或者超過平臺最大字長(word size)的訪問。
任意數量的指令操作都可被表示成這個表格中的第一個操作或者第二個操作。例如在單元格[Normal Store, Volatile Store]中,有一個No,就表示任何非volatile欄位的store指令操作不能與後面任何一個Volatile store指令重排, 如果出現任何這樣的重排會使多執行緒程式的執行發生變化。
JSR-133規範規定上述關於volatile和監視器的規則僅僅適用於可能會被多執行緒訪問的變數或物件。因此,如果一個編譯器可以最終證明(往往是需要很大的努力)一個鎖只被單執行緒訪問,那麼這個鎖就可以被去除。與之類似的,一個volatile變數只被單執行緒訪問也可以被當作是普通的變數。還有進一步更細粒度的分析與優化,例如:那些被證明在一段時間內對多執行緒不可訪問的欄位。
在上表中,空白的單元格代表在不違反Java的基本語義下的重排是允許的(詳細可參考JLS中的說明)。例如,即使上表中沒有說明,但是也不能對同一個記憶體地址上的load指令和之後緊跟著的store指令進行重排。但是你可以對兩個不同的記憶體地址上的load和store指令進行重排,而且往往在很多編譯器轉換和優化中會這麼做。這其中就包括了一些往往不認為是指令重排的例子,例如:重用一個基於已經載入的欄位的計算後的值而不是像一次指令重排那樣去重新載入並且重新計算。然而,JMM規範允許編譯器經過一些轉換後消除這些可以避免的依賴,使其可以支援指令重排。
在任何的情況下,即使是程式設計師錯誤的使用了同步讀取,指令重排的結果也必須達到最基本的Java安全要求。所有的顯式欄位都必須不是被設定成0或null這樣的預構造值,就是被其他執行緒設值。這通常必須把所有儲存在堆記憶體裡的物件在其被建構函式使用前進行歸零操作,並且從來不對歸零store指令進行重排。一種比較好的方式是在垃圾回收中對回收的記憶體進行歸零操作。可以參考JSR-133規範中其他情況下的一些關於安全保證的規則。
這裡描述的規則和屬性都是適用於讀取Java環境中的欄位。在實際的應用中,這些都可能會另外與讀取內部的一些記賬欄位和資料互動,例如物件頭,GC表和動態生成的程式碼。
Final 欄位
Final欄位的load和store指令相對於有鎖的或者volatile欄位來說,就跟Normal load和Normal store的存取是一樣的,但是需要加入兩條附加的指令重排規則:
- 如果在建構函式中有一條final欄位的store指令,同時這個欄位是一個引用,那麼它將不能與建構函式外後續可以讓持有這個final欄位的物件被其他執行緒訪問的指令重排。例如:你不能重排下列語句:
x.finalField = v; ... ; sharedRef = x;
這條規則會在下列情況下生效,例如當你內聯一個建構函式時,正如“…”的部分表示這個建構函式的邏輯邊界那樣。你不能把這個建構函式中的對於這個final欄位的store指令移動到建構函式外的一條store指令後面,因為這可能會使這個物件對其他執行緒可見。(正如你將在下面看到的,這樣的操作可能還需要宣告一個記憶體屏障)。類似的,你不能把下面的前兩條指令與第三條指令進行重排:
x.afield = 1; x.finalField = v; ... ; sharedRef = x;
- 一個final欄位的初始化load指令不能與包含該欄位的物件的初始化load指令進行重排。在下面這種情況下,這條規則就會生效:x = shareRef; … ; i = x.finalField;
由於這兩條指令是依賴的,編譯器將不會對這樣的指令進行重排。但是,這條規則會對某些處理器有影響。
上述規則,要求對於帶有final欄位的物件的load本身是synchronized,volatile,final或者來自類似的load指令,從而確保java程式設計師對與final欄位的正確使用,並最終使建構函式中初始化的store指令和建構函式外的store指令排序。
—————————————————————————————————————————–
Reorderings
For a compiler writer, the JMM mainly consists of rules disallowing reorderings of certain instructions that access fields (where “fields” include array elements) as well as monitors (locks).
Volatiles and Monitors
The main JMM rules for volatiles and monitors can be viewed as a matrix with cells indicating that you cannot reorder instructions associated with particular sequences of bytecodes. This table is not itself the JMM specification; it is just a useful way of viewing its main consequences for compilers and runtime systems.
Can Reorder | 2nd operation | ||
1st operation | Normal Load Normal Store |
Volatile load MonitorEnter |
Volatile store MonitorExit |
Normal Load Normal Store |
No | ||
Volatile load MonitorEnter |
No | No | No |
Volatile store MonitorExit |
No | No |
Where:
- Normal Loads are getfield, getstatic, array load of non-volatile fields.
- Normal Stores are putfield, putstatic, array store of non-volatile fields
- Volatile Loads are getfield, getstatic of volatile fields that are accessible by multiple threads
- Volatile Stores are putfield, putstatic of volatile fields that are accessible by multiple threads
- MonitorEnters (including entry to synchronized methods) are for lock objects accessible by multiple threads.
- MonitorExits (including exit from synchronized methods) are for lock objects accessible by multiple threads.
The cells for Normal Loads are the same as for Normal Stores, those for Volatile Loads are the same as MonitorEnter, and those for Volatile Stores are same as MonitorExit, so they are collapsed together here (but are expanded out as needed in subsequent tables). We consider here only variables that are readable and writable as an atomic unit — that is, no bit fields, unaligned accesses, or accesses larger than word sizes available on a platform.
Any number of other operations might be present between the indicated 1st and 2nd operations in the table. So, for example, the “No” in cell [Normal Store, Volatile Store] says that a non-volatile store cannot be reordered with ANY subsequent volatile store; at least any that can make a difference in multithreaded program semantics.
The JSR-133 specification is worded such that the rules for both volatiles and monitors apply only to those that may be accessed by multiple threads. If a compiler can somehow (usually only with great effort) prove that a lock is only accessible from a single thread, it may be eliminated. Similarly, a volatile field provably accessible from only a single thread acts as a normal field. More fine-grained analyses and optimizations are also possible, for example, those relying on provable inaccessibility from multiple threads only during certain intervals.
Blank cells in the table mean that the reordering is allowed if the accesses aren’t otherwise dependent with respect to basic Java semantics (as specified in theJLS). For example even though the table doesn’t say so, you can’t reorder a load with a subsequent store to the same location. But you can reorder a load and store to two distinct locations, and may wish to do so in the course of various compiler transformations and optimizations. This includes cases that aren’t usually thought of as reorderings; for example reusing a computed value based on a loaded field rather than reloading and recomputing the value acts as a reordering. However, the JMM spec permits transformations that eliminate avoidable dependencies, and in turn allow reorderings.
In all cases, permitted reorderings must maintain minimal Java safety properties even when accesses are incorrectly synchronized by programmers: All observed field values must be either the default zero/null “pre-construction” values, or those written by some thread. This usually entails zeroing all heap memory holding objects before it is used in constructors and never reordering other loads with the zeroing stores. A good way to do this is to zero out reclaimed memory within the garbage collector. See the JSR-133 spec for rules dealing with other corner cases surrounding safety guarantees.
The rules and properties described here are for accesses to Java-level fields. In practice, these will additionally interact with accesses to internal bookkeeping fields and data, for example object headers, GC tables, and dynamically generated code.
Final Fields
Loads and Stores of final fields act as “normal” accesses with respect to locks and volatiles, but impose two additional reordering rules:
- A store of a final field (inside a constructor) and, if the field is a reference, any store that this final can reference, cannot be reordered with a subsequent store (outside that constructor) of the reference to the object holding that field into a variable accessible to other threads. For example, you cannot reorder
x.finalField = v; ... ; sharedRef = x;
This comes into play for example when inlining constructors, where “...” spans the logical end of the constructor. You cannot move stores of finals within constructors down below a store outside of the constructor that might make the object visible to other threads. (As seen below, this may also require issuing a barrier). Similarly, you cannot reorder either of the first two with the third assignment in:
v.afield = 1; x.finalField = v; ... ; sharedRef = x; - The initial load (i.e., the very first encounter by a thread) of a final field cannot be reordered with the initial load of the reference to the object containing the final field. This comes into play in:
x = sharedRef; ... ; i = x.finalField;
A compiler would never reorder these since they are dependent, but there can be consequences of this rule on some processors.
These rules imply that reliable use of final fields by Java programmers requires that the load of a shared reference to an object with a final field itself be synchronized, volatile, or final, or derived from such a load, thus ultimately ordering the initializing stores in constructors with subsequent uses outside constructors.