Java內(nèi)存模型是通過各種操作來定義的,包括對變量的讀/寫操作匆笤,監(jiān)視器的加鎖和釋放操作研侣,以及線程的啟動和合并操作。Java 內(nèi)存模型為程序中所有的操作定義了一個(gè)偏序關(guān)系炮捧,稱之為 Happens-Before 义辕。在并發(fā)編程中,這個(gè)概念尤為重要寓盗。
偏序關(guān)系
偏序關(guān)系?是離散數(shù)學(xué)中集合上的一種關(guān)系灌砖,具有反對稱璧函、自反和傳遞屬性。但對于任意兩個(gè)元素x基显,y來說蘸吓,并不需要一定滿足 x?y 或 y?x 的關(guān)系。例如我們在表達(dá)喜好的時(shí)候撩幽,可以說更喜歡橙子而不是蘋果库继,可以說是更喜歡張學(xué)友而不是樸樹,但我們不必在蘋果和樸樹之間做出明確的喜好選擇窜醉。
在Java中宪萄,如果想保證執(zhí)行操作B的線程看到操作A的結(jié)果(無論A和B是否在同一個(gè)線程中執(zhí)行),那么A和B之間必須滿足 Happens-Before 關(guān)系榨惰。如果兩個(gè)操作之間缺乏 Happens-Before 關(guān)系拜英,那么JVM可以對它們?nèi)我獾刂嘏判颉Ee一個(gè)簡單的例子琅催,當(dāng)兩個(gè)線程使用同一個(gè)鎖進(jìn)行同步時(shí)居凶,它們之間的 Happens-Before 關(guān)系如下:
當(dāng)一個(gè)變量被多個(gè)線程讀取并且至少被一個(gè)線程寫入時(shí),如果讀操作和寫操作之間沒有依照 Happens-Before 來排序藤抡,那么就會產(chǎn)生數(shù)據(jù)競爭問題侠碧。在正確同步的程序中不存在數(shù)據(jù)競爭,并會表現(xiàn)出串行一致性缠黍,這意味著程序中的所有操作都會按照一種固定的和全局的順序執(zhí)行弄兜。
Happens-Before規(guī)則
- 程序順序規(guī)則:如果程序中操作A在操作B之前,那么在線程中A操作將在B操作之前執(zhí)行瓷式。
- 監(jiān)視器鎖規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個(gè)監(jiān)視器鎖上的加鎖操作之前執(zhí)行替饿。顯示鎖Lock和內(nèi)置鎖synchronnized在加鎖和解鎖等操作上都有著相同的內(nèi)存語義。
- volatile 變量規(guī)則:對 volatile 變量的寫入操作必須在對該變量的讀操作之前執(zhí)行蒿往。原子變量在讀/寫操作上也有著相同的語義盛垦。
- 線程啟動規(guī)則:在線程上對 Thread.start 的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行。
- 線程結(jié)束規(guī)則:線程中的任何操作都必須在其他線程檢測到該線程已經(jīng)結(jié)束之前執(zhí)行瓤漏,或者從 Thread.join 中成功返回腾夯,或者在調(diào)用 Thread.isAlive 時(shí)返回false。
- 線程中斷規(guī)則:當(dāng)一個(gè)線程在另一個(gè)線程上調(diào)用 interrupt 時(shí)蔬充,必須在被中斷線程檢測到 interrupt 調(diào)用之前執(zhí)行(通過拋出 InterruptedException蝶俱,或者調(diào)用 isInterrupted 和 interrupted)。
- 對象終結(jié)規(guī)則:對象的構(gòu)造函數(shù)必須在啟動該對象的終結(jié)器之前執(zhí)行完成饥漫。
- 傳遞性:如果操作A在操作B之前執(zhí)行榨呆,并且操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行庸队。
應(yīng)用
在Java并發(fā)包(java.util.concurrent)中积蜻,基本上所有的類都借助了 Happens-Before 的程序規(guī)則闯割。例如在AQS(AbstractQueuedSynchronnizer)設(shè)計(jì)中,必須要確保調(diào)用tryAcquireShared
之前總能成功調(diào)用tryReleaseShared
竿拆≈胬或者是在ConcurrentHashMap中,get(key)
和相關(guān)的訪問方法返回的任何非空結(jié)果都會有一個(gè)與之關(guān)聯(lián)的插入或者更新事件丙笋。
實(shí)際應(yīng)用中谢澈,舉一個(gè)最簡單的單例模式例子,看下面的代碼:
public class UnsafeLazyInitiallization{
private static Resource resource;
public static Resource getInstance(){
if (resource == null)
resource = new Resource();
return resource;
}
}
上面這段代碼在單線程環(huán)境中可以正常運(yùn)行御板,但如果在多線程環(huán)境中锥忿,假設(shè)A線程是第一個(gè)調(diào)用getInstance
的線程,它將看到resource為null怠肋,然后為設(shè)置resource一個(gè)新的實(shí)例敬鬓。然后線程B調(diào)用getInstance
,獲得線程A設(shè)置的新的實(shí)例灶似。當(dāng)然列林,這是理想情況下的可能發(fā)生的情況瑞你,但是需要注意酪惭,線程A寫入resource的操作與線程B讀取resource的操作之間不存在 Happens-Before 關(guān)系,如果在發(fā)布對象時(shí)存在數(shù)據(jù)競爭問題者甲,線程B就不一定能看到resource的正確狀態(tài)春感。我們可以利用監(jiān)視器鎖規(guī)則來對它加以改進(jìn):
public class SafeLazyInitiallization{
private static Resource resource;
public synchronized static Resource getInstance(){
if (resource == null)
resource = new Resource();
return resource;
}
}
使用內(nèi)置鎖 synchronized,可以在并發(fā)時(shí)確保只有一個(gè)線程進(jìn)行resource的初始化操作虏缸,這樣鲫懒,進(jìn)入synchronized代碼段的線程與其他線程就可以保持 Happens-Before 關(guān)系。