并發(fā)三問題
- 重排序
- 內(nèi)存可見性
- 原子性
1. 重排序
public class Test {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
CountDownLatch latch = new CountDownLatch(1);
Thread one = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
a = 1;
x = b;
});
Thread other = new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) {
}
b = 1;
y = a;
});
one.start();other.start();
latch.countDown();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
觀察代碼可以發(fā)現(xiàn)甜橱,如果沒有意外情況發(fā)生的話佃牛,在上下兩個線程中略板,出現(xiàn)的結(jié)果應該下面三種情況
x= 0 ,y = 1;
</p>
x= 1 ,y = 1;
</p>
x= 1 ,y = 0;
但是在實際運行過程中洼哎,卻最終有概率出現(xiàn) x=0,y=0的情況。這種情況發(fā)生的原因就是出現(xiàn)了重排序让簿。
重排序由一下幾種機制引起:</p>
- 編譯器優(yōu)化:對于沒有數(shù)據(jù)依賴關(guān)系的操作,編譯器在編譯的過程中會進行一定程度的重排秀睛。</p>
可以看到線程1中的代碼尔当,編譯器是可以將a=1和x=b換一下順序的,因為它們之間沒有數(shù)據(jù)依賴關(guān)系,同理,線程2也一樣凭涂,那就不難得到x==y==0的結(jié)果了。
</p> - 指令重排序:CPU優(yōu)化行為畜号,也是會對不存在數(shù)據(jù)依賴關(guān)系的指令進行一定程度的重排</p>
這個和編譯器優(yōu)化差不多,就算編譯器不發(fā)生重排缠黍,CPU也可以對指令進行重排弄兜。
</p> - 內(nèi)存系統(tǒng)重排序:內(nèi)存系統(tǒng)沒有重排序药蜻,但是由于緩存的存在瓷式,使得程序整體上會表現(xiàn)出亂序的行為。</p>
假設不發(fā)生編譯器重排和指令重排语泽,線程1修改了a的值贸典,但是修改以后,a的值可能還沒寫回到主內(nèi)存中踱卵,那么線程2得到a==0就是很自然的事了廊驼。同理,線程2對于b的賦值操作也可能沒有及時刷新到主存中惋砂。
2.內(nèi)存可見性
線程間的對于共享變量的可見性問題不是直接由多核引起的妒挎,而是由多緩存引起的。如果每個核心共享同一個緩存西饵,那么也就不存在內(nèi)存可見性問題了酝掩。 </p>
現(xiàn)代多核CPU中每個核心擁有自己的一級緩存或一級緩存加上二級緩存等,問題就發(fā)生在每個核心的獨占緩存上眷柔。每個核心都將會自己需要的數(shù)據(jù)讀到獨占緩存中期虾,數(shù)據(jù)修改后也是寫入到緩存中,然后等待刷入到主存中驯嘱。所以會導致有些核心讀取的值是一個過期的值镶苞。</p>
在JMM中,抽象了主內(nèi)存和本地內(nèi)存的概念鞠评。</p>
所有的共享變量存在于主內(nèi)存中茂蚓,每個線程有自己的本地內(nèi)存,線程讀寫共享數(shù)據(jù)也是通過本地內(nèi)存交換,所以可見性問題依然存在煌贴。這里說的本地緩存并不是真的是一塊給每個線程分配的內(nèi)存御板,而是JMM的一個抽象,是對于寄存器牛郑、一級緩存怠肋、二級緩存等的抽象。
3.原子性
對于long和double淹朋,它們的值都需要占用64位的內(nèi)存空間笙各,Java編程語言規(guī)范中提到,對于64位的值的寫入础芍,可以分為兩個32位的操作進行寫入杈抢。本來一個整體的賦值操作,將被拆分為低32位賦值和高32位賦值兩個操作仑性,中間如果發(fā)生了其他線程對于這個值的讀操作惶楼,必然會讀到一個奇怪的值。</p>
這個時候我們需要使用volatile關(guān)鍵字進行控制了诊杆,JMM規(guī)定了對于volatile long 和volatile double歼捐,JVM需要保證寫入操作的原子性。</p>
另外晨汹,對于引用的讀寫操作始終是原子的豹储,不管是32位的機器還是64位的機器。</p>
Java編程規(guī)范同樣提到淘这,鼓勵JVM的開發(fā)者能保證64位值操作的原子性剥扣,也鼓勵使用盡量使用volatile或使用正確的同步方式。關(guān)鍵詞是“鼓勵”铝穷。</p>
在64位JVM中钠怯,不加volatile也是可以的,同樣能保證對于long和double寫操作的原子性曙聂。
Java對于并發(fā)的規(guī)范約束
Synchronization Order
- 對于監(jiān)視器m的解鎖與所有后續(xù)操作對于m的加鎖同步
- 對于volatile變量v的寫入晦炊,與所有其他線程后續(xù)對v的讀同步
- 啟動線程的操作與線程職工的第一個操作同步
- 對于每個屬性寫入默認值(0,false,null)與每個線程對其進行的操作同步。
Happens-before Order
兩個操作可以用Happens-before來確定它們的執(zhí)行順序筹陵,如果一個操作happens-before于另一個操作刽锤,那么我們說第一個操作對于第二個操作是可見的。</p>
如果我們分別有操作X和操作Y朦佩,我們寫成hb(x,y)并思,來表示 x happens-before y 。</p>
- 如果操作x和操作y是同一個線程的兩個操作语稠,并且在代碼上操作x先于操作y出現(xiàn)宋彼,那么有hb(x,y)弄砍。
- 對象構(gòu)造方法的最后一行指令happens-before于finalize()方法的第一行指令。
- 對于操作x與隨后的操作y構(gòu)成同步输涕,那么hb(x,y)
- hb(x,y)和hb(y,z)音婶,那么可以推斷出hb(x,z)
這里需要說明的是:hb(x,y),并不是說x操作一定要在y操作之前被執(zhí)行,而是說x的執(zhí)行結(jié)果對于y是可見的莱坎,只要滿足可見性衣式,發(fā)生了重排序也是可以的。
synchronized關(guān)鍵字
一個線程獲取到鎖以后才能進入synchronized控制的代碼塊檐什,一旦進入代碼塊碴卧,首先,該線程對于共享變量的緩存就會失效
乃正,因此synchornized代碼塊中對于共享變量的讀取需要從主內(nèi)存中重新獲取住册,也就能獲取到最新的值。</p>
退出代碼塊的時候瓮具,會將該線程寫緩沖區(qū)的數(shù)據(jù)刷到主內(nèi)存中荧飞。