JDK5引入了JMM新規范:JSR-133,引入了happens-before/可見性等概念,對synchronized/volatile/final等關鍵詞進行了語義定義。解決了:final變量在構造器中初始化的線程安全問題以及volatile變量與no-volatile變量之間的重排序問題。
為什麼需要Memory Model
在多線程的場景下,thread1修改了一個變量後,thread2要讀取這個變量,其間可能會發生指令執行順序的問題(因為編譯器優化指令、處理器重排指令、寫數據緩存未及時更新到主內存)。如何保證thread2要讀的變量是想要的thread1修改後的變量呢?
Memory Model 描述了程序中變量以及它們在寄存器、緩存、內存中的關系的問題。即,在沒有Momory Model 的情況下,無法保證多線程環境下變量調用的次序問題,有了Memory Model的具體關鍵詞對應的語義定義(比如synchronized/volatile/final),就可以使用這些關鍵詞來保證程序是按照想要的邏輯在多線程環境下正確執行。
舊有JMM的問題
問題#1:不可變(Immutable)對象並不是不可變的
不可變對象:對象的所有字段必須是final的,並且必須是基元類型或者是不可變對象的引用。比如,String對象是不可變的,語義上來說應該不需要 synchronization,但事實上是,多線程場景下有可能有競爭條件。
@JKD1.4
class String
{
static final char[] charArray;
static int length;
static int offset; // 表示字符串的開始位置
}
charArray數組以及length、offset可以在多個String/StringBuffer中共享。比如 String.substring()就是共享了原有String的charArray。
String s1 = "/usr/tmp";
String s2 = s1.substring(4); // contains "/tmp"
在舊有JMM中,沒有synchronization。初始化s1的時,object的構造器將length/offset初始化為0,此時其他線程可以訪問這些值(這個時候使用substring明顯就有問題),接下來String的構造器為length/offset賦值為需要的值。新JMM模型解決了final變量在構造器中初始化時的線程安全問題,即:final變量在初始化之前(構造函數執行完畢之前)是不允許其他線程可見的。
問題#2:volatile變量與非volatile變量的重排序
volatile變量在舊有JMM僅有的一個語義:對於一個volatile變量的讀,總是能看到其它任意線程對個這個volatile變量的最後寫入。即,對volatile變量的寫 happends-before 其他線程對其讀。
在舊有JMM中,雖然不允許volatile變量之間的重排序,但允許volatile變量與其它普通變量之間重排序。這就導致了在把volatile變量作為標志位的場景下出現問題。
Map configOptions;
char[] configText;
volatile boolean initialized = false;
. . .
// In thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
. . .
// In thread B
while (!initialized)
sleep();
// use configOptions ,在舊有JMM中,這裡並不保證configOptions已經初始化,因為變量順序可能已經重排。
新JMM模型解決了這個問題,引入了新語義:volatile變量不能與非volatile變量發生重排。
新的JMM
Visibility 可見性
當ThreadA執行 val = 1 後,其它線程如何能夠確保看到ThreadA的執行結果(即變量此時對其它線程的Visibility)?JMM規范中的某些定義(volatile/synchronized/final)來確保這件事情。
synchronized 與可見性
synchronized 不僅有進入一個臨界區鎖定的語義,同時還具有內存可見性(memory visibility)方面的意義。離開synchronized塊時,cache必須flush到主內存當中;進入synchronized塊時,cache失效,隨後的讀操作必須到主內存當中讀。即,synchronized塊裡面的變量寫操作對其他線程是可見(visible)的!
Volatile
新的JMM增強了volatile的內存語義,禁止與普通變量重排。即,threadA對一個 volatile 變量 write ,threadB讀取那個變量。那麼,對A可見的所有變量,必須同時對B可見。
happens-before
普通的多線程如果沒有任何數據共享和交互,則指令可能會因為優化的緣故進行重排,也不會造成影響。如果線程之間有數據競爭,則需要使用synchronized(Moniter)/volatile等來保證指令的執行順序。JMM定義了一種排序叫“happens-before”,使用該概念,JMM來解釋什麼叫做內存可見性。如果一個操作A的結果對另外一個操作B是內存可見的,則A happends-before B。
* 一個線程中,靠前的操作 happends-before 後續操作。
* 對monitor的unlock happends-before 後續的加鎖操作。
* 對volatile變量的寫操作 happends-before 後續的讀。
* Thread.start()的調用操作 happends-before 線程內部操作。
* 線程的所有操作都 happends-before 後續的線程join()。
Data races —— 數據競爭
存在數據競爭,說明該程序沒有進行良好的同步,沒有處理好 happends-before 關系。
final變量的初始化安全
對象只有在完全初始化後,其final變量對其它線程才是可見的。這說明,final變量的初始化可以在不使用synchronization的情況下實現線程安全。這樣,final變量實現了真正意義上的final(即,不-可-變),而不會在初始化過程中、後在多線程中看起來會改變。
class A
{
final Map<String,String> map=null;
public void A()
{
map = new HashMap<String,String>();
map.put("key1","value1");
}
}
其他線程在引用A的對象之前,JMM保證A的final變量已經被其構造函數完全初始化。也就是說,final變量的完全初始化 happends-before 其他線程對該對象的引用。即,final變量在構造函數中的初始化是線程安全的。