理解happens-before主要為了理解源碼。主要jdk里面一堆華麗呼哨的操作涂籽,如果基礎(chǔ)不牢靠吕喘,看著心累。
目錄:
1.happens-before的理解
2.應(yīng)用1矮固,futuretask non-volatile
引用:
1.why outcome object in FutureTask is non-volatile? (老外對(duì)代碼的理解真的厲害)
2.happens-before俗解
3. Java內(nèi)存模型之happens-before
4. 一句話理解什么是happens-before
5. happen-before規(guī)則及其對(duì)DCL的分析(含代碼)
1.happens-before的理解
1.1 為什么要有一個(gè)happens-before的原則失息?
結(jié)論:happens-before覺得著什么時(shí)候變量操作對(duì)你可見。
我們知道cpu的運(yùn)行極快,而讀取主存對(duì)于cpu而言有點(diǎn)慢了盹兢,在讀取主存的過(guò)程中cpu一直閑著(也沒數(shù)據(jù)可以運(yùn)行)邻梆,這對(duì)資源來(lái)說(shuō)造成極大的浪費(fèi)。所以慢慢的cpu演變成了多級(jí)cache結(jié)構(gòu)绎秒,cpu在讀cache的速度比讀內(nèi)存快了n倍浦妄。
當(dāng)線程在執(zhí)行時(shí),會(huì)保存臨界資源的副本到私有work memory中见芹,這個(gè)memory在cache中剂娄,修改這個(gè)臨界資源會(huì)更新work memory但并不一定立刻刷到主存中,那么什么時(shí)候應(yīng)該刷到主存中呢辆童?什么時(shí)候和其他副本同步宜咒?
而且編譯器為了提高指令執(zhí)行效率,是可以對(duì)指令重排序的把鉴,重排序后指令的執(zhí)行順序不一樣故黑,有可能線程2讀取某個(gè)變量時(shí),線程1還未進(jìn)行寫入操作庭砍。這就是線程可見性的來(lái)源场晶。
針對(duì)以上兩個(gè)問(wèn)題,JMM給出happens-before通用的規(guī)則(注意這僅對(duì)java而言怠缸,其他的就布吉島了)
1.2 happens-before原則有啥好處诗轻?
i = 1; //線程A執(zhí)行
j = i ; //線程B執(zhí)行
j 是否等于1呢?假定線程A的操作(i = 1)happens-before線程B的操作(j = i)揭北。
那么可以確定線程B執(zhí)行后j = 1 一定成立扳炬。
如果他們不存在happens-before原則,那么j = 1 不一定成立搔体。
(即使代碼是先執(zhí)行j=1,然后執(zhí)行j=i恨樟,也不一定j=1,主要看是否符合happens-before)
1.3 happens-before原則
- 如果操作1 happens-before 操作2,那么第操作1的執(zhí)行結(jié)果將對(duì)操作2可見疚俱,而且操作1的執(zhí)行順序排在第操作2之前劝术。
- 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著一定要按照happens-before原則制定的順序來(lái)執(zhí)行呆奕。如果重排序之后的執(zhí)行結(jié)果與按照happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致养晋,那么這種重排序并不非法。
1.4如何判斷是否為 happens-before梁钾?
程序次序規(guī)則: 在一個(gè)單獨(dú)的線程中绳泉,按照程序代碼的執(zhí)行流順序,(時(shí)間上)先執(zhí)行的操作happen—before(時(shí)間上)后執(zhí)行的操作
(同一個(gè)線程中前面的所有寫操作對(duì)后面的操作可見)管理鎖定規(guī)則:一個(gè)unlock操作happen—before后面(時(shí)間上的先后順序)對(duì)同一個(gè)鎖的lock操作陈轿。
(如果線程1解鎖了monitor a圈纺,接著線程2鎖定了a秦忿,那么麦射,線程1解鎖a之前的寫操作都對(duì)線程2可見(線程1和線程2可以是同一個(gè)線程))volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫操作happen—before后面(時(shí)間上)對(duì)該變量的讀操作蛾娶。
(如果線程1寫入了volatile變量v(臨界資源),接著線程2讀取了v潜秋,那么蛔琅,線程1寫入v及之前的寫操作都對(duì)線程2可見(線程1和線程2可以是同一個(gè)線程))線程啟動(dòng)規(guī)則:Thread.start()方法happen—before調(diào)用用start的線程前的每一個(gè)操作钠至。
(假定線程A在執(zhí)行過(guò)程中挂脑,通過(guò)執(zhí)行ThreadB.start()來(lái)啟動(dòng)線程B,那么線程A對(duì)共享變量的修改在接下來(lái)線程B開始執(zhí)行前對(duì)線程B可見奖恰。注意:線程B啟動(dòng)之后钩述,線程A在對(duì)變量修改線程B未必可見寨躁。)線程終止規(guī)則:線程的所有操作都happen—before對(duì)此線程的終止檢測(cè),可以通過(guò)Thread.join()方法結(jié)束牙勘、Thread.isAlive()的返回值等手段檢測(cè)到線程已經(jīng)終止執(zhí)行职恳。
(線程t1寫入的所有變量,在任意其它線程t2調(diào)用t1.join()方面,或者t1.isAlive() 成功返回后放钦,都對(duì)t2可見。)線程中斷規(guī)則:對(duì)線程interrupt()的調(diào)用 happen—before 發(fā)生于被中斷線程的代碼檢測(cè)到中斷時(shí)事件的發(fā)生恭金。
(線程t1寫入的所有變量操禀,調(diào)用Thread.interrupt(),被打斷的線程t2横腿,可以看到t1的全部操作)對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)happen—before它的finalize()方法的開始颓屑。
(對(duì)象調(diào)用finalize()方法時(shí),對(duì)象初始化完成的任意操作耿焊,同步到全部主存同步到全部cache揪惦。)傳遞性:如果操作A happen—before操作B,操作B happen—before操作C搀别,那么可以得出A happen—before操作C丹擎。
(A h-b B , B h-b C 那么可以得到 A h-b C)
1.5 一言以蔽之歇父,這些規(guī)則背后的道理
在程序運(yùn)行過(guò)程中蒂培,所有的變更會(huì)先在寄存器或本地cache中完成,然后才會(huì)被拷貝到主存以跨越內(nèi)存柵欄(本地或工作內(nèi)存到主存之間的拷貝動(dòng)作)榜苫,此種跨越序列或順序稱為happens-before护戳。
注:happens-before本質(zhì)是順序,重點(diǎn)是跨越內(nèi)存柵欄
通常情況下垂睬,寫操作必須要happens-before讀操作媳荒,即寫線程需要在所有讀線程跨越內(nèi)存柵欄之前完成自己的跨越動(dòng)作抗悍,其所做的變更才能對(duì)其他線程可見。
2.應(yīng)用
2.1 單例模式
單例模式可能存在問(wèn)題哦钳枕,請(qǐng)看我的文章【單例模式】DCL的問(wèn)題和解決方法
可以看出缴渊,如果有兩個(gè)線程都執(zhí)行過(guò)synchronized ,那么符合"管理鎖定規(guī)則"鱼炒,那么我們可以線程 singleton即使不加上volatile衔沼,也不會(huì)影響線程間的可見性
public class Singleton {
private static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
Singleton temp = null;
try {
temp = new Singleton();
} catch (Exception e) { }
if (temp != null)
singleton = temp;
}
}
}
return singleton;
}
}
2.2 CopyOnWriteArrayList 的例子
線程A和線程B要執(zhí)行的以下代碼,最后結(jié)果b=1嗎昔瞧?(其中l(wèi)ist為CopyOnWriteArrayList)
線程A | 線程B |
---|---|
a = 1; | list.get(0); |
list.set(1,""); | int b = a; |
執(zhí)行順序流1:
步驟 | 線程A | 線程B |
---|---|---|
a | a = 1; | |
b | list.set(1,""); | |
c | list.get(0); | |
d | int b = a; |
執(zhí)行順序流2:
步驟 | 線程A | 線程B |
---|---|---|
a | a = 1; | |
b | list.get(0); | |
c | list.set(1,""); | |
d | int b = a; |
在確線程B是否一定能看到線程A的a變量前指蚁,我們先看看CopyOnWriteArrayList 的源碼:
可以發(fā)現(xiàn)基本get/set都是一個(gè)volatile申明的array變量
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//TODO xxx
setArray(newElements);
} finally {
lock.unlock();
}
final void setArray(Object[] a) {
array = a;
}
通過(guò)我們的源碼的分析,基本可以判斷這里要用到volatile變量規(guī)則自晰,
即:對(duì)一個(gè)volatile變量的寫操作happen—before后面(時(shí)間上)對(duì)該變量的讀操作凝化。
我們對(duì)執(zhí)行順序流進(jìn)行分析:
(步驟a happens-before 步驟b 記為 hb(a,b))
順序流1:
根據(jù)程序次序規(guī)則可以得到 hb(a,b),hb(c,d)酬荞,如果我們希望b=1搓劫,那么只需要 hb(b,c)
由于volatile變量規(guī)則,我們可以得到hb(b,c)袜蚕,所以一定b=1糟把。
順序流2:
根據(jù)程序次序規(guī)則可以得到 hb(a,c),hb(b,d)牲剃,如果我們希望b=1遣疯,那么我們需要hb(a,b)或hb(c,d)。
然而沒有規(guī)則可以得到以上條件凿傅,故不成立缠犀,b不一定等于1。