Java內(nèi)存模型 HB法則
概述
- HB 7法則:volatile帽揪、start線程開(kāi)始戏罢、join線程結(jié)束屋谭、lock鎖釋放之后、finalize對(duì)象銷毀龟糕,時(shí)間先后桐磁、傳遞性
- java內(nèi)存模型:每個(gè)線程操作數(shù)據(jù)是把對(duì)象拷貝到本地內(nèi)存然進(jìn)行操作,最后寫(xiě)回到主內(nèi)存當(dāng)中讲岁,不同線程之間不可見(jiàn)
- JMM為了防止重排序引發(fā)問(wèn)題我擂,保證了happens-before法則,這些法則是不會(huì)被重排序打亂的
- java的原子性:一個(gè)操作是原子操作缓艳,那么我們稱它具有原子性校摩,JDK1.5后再JUC包下有AotmicInteger來(lái)簡(jiǎn)化并發(fā)的原子操作,讀取 操作 復(fù)查 寫(xiě)入的一個(gè)過(guò)程阶淘。多了一個(gè)可見(jiàn)性的復(fù)查過(guò)程避免線程不安全衙吩。unsafe類提供本地方法直接操作內(nèi)存,從硬件方面實(shí)現(xiàn)原子性溪窒。
happens-before原則:
7個(gè)場(chǎng)景總結(jié):
- 子線程.start()
- 子線程.join()
- volatile
- lock
- 傳遞性
- 初始化
- 單一線程前后性
Each action in a thread happens-before every subsequent action in that thread.
在一個(gè)線程當(dāng)中 先執(zhí)行的代碼結(jié)果 對(duì)隨后進(jìn)行的代碼可見(jiàn)坤塞。(廢話,一個(gè)線程公用一個(gè)本地緩存)An unlock on a monitor happens-before every subsequent lock on that monitor
對(duì)一個(gè)監(jiān)視器進(jìn)行解鎖時(shí)的所有操作澈蚌,對(duì)下一個(gè)鎖的代碼是可見(jiàn)的摹芙,比如在線程A中改了共有變量n的值,現(xiàn)在線程B獲取了相同的鎖宛瞄,它是可以看見(jiàn)改變后n的值的浮禾。A write to a volatile field happens-before every subsequent read of that volatile.
A改了一個(gè)volatile的值,對(duì)接下來(lái)的所有的讀取該變量值的線程可見(jiàn)A可見(jiàn)于B B可見(jiàn)于C,那么A的所有操作也可見(jiàn)于C
start規(guī)則:如果線程A執(zhí)行線程B的start方法盈电,那么線程A的ThreadB.start()happens-before于線程B的任意操作
A執(zhí)行的操作之后再啟動(dòng)B線程蝴簇,則A線程之前改過(guò)的值對(duì)B可見(jiàn)
static int index=1;
index++挣轨;
new Thread(new runnable(){
public void run(){
system.out.println(index);
}
}).start();
- join規(guī)則:如果線程A執(zhí)行線程B的join方法军熏,那么線程B的任意操作happens-before于線程A從TreadB.join()方法成功返回
main(){
t1.start()
t1.join()
ti修改過(guò)的變量對(duì)main線程可見(jiàn)
}
- 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始;
可見(jiàn)性問(wèn)題
基于java的內(nèi)存模型卷扮,不同線程操作相同變量的時(shí)候荡澎,因?yàn)榫€程之間不可見(jiàn),會(huì)導(dǎo)致可見(jiàn)性問(wèn)題晤锹。
舉例:
主內(nèi)存中有變量count=100(比如某個(gè)對(duì)象的成員變量為count是100)摩幔,現(xiàn)在多個(gè)線程去消費(fèi)這個(gè)對(duì)象的count。
線程A讀取count=100鞭铆,進(jìn)行了count消費(fèi)一個(gè)或衡,修改本地內(nèi)存為99,這個(gè)時(shí)候并不會(huì)直接寫(xiě)回到主內(nèi)存當(dāng)中车遂,此時(shí)線程B去主內(nèi)存中讀取并消費(fèi)count封断,最后線程A結(jié)束,寫(xiě)回count為99到主內(nèi)存舶担,線程B結(jié)束的時(shí)候坡疼,也寫(xiě)回99到主內(nèi)存,因?yàn)榭梢?jiàn)性問(wèn)題衣陶,導(dǎo)致count被消費(fèi)2次柄瑰,但是總量只減少了1。解決方案(volatile關(guān)鍵字剪况、synchronized教沾、final)
- synchronized可以保證線程安全,但是效率低
- volatile不能保證線程安全译断,但是可以緩解線程不可見(jiàn)的問(wèn)題授翻,比如A消費(fèi)了count從100到99的時(shí)候,會(huì)立刻把count寫(xiě)回到主內(nèi)存孙咪,如果B線程這個(gè)時(shí)候去取count這個(gè)對(duì)象藏姐,取到的就是99,而不是100该贾。
- final關(guān)鍵字編譯的時(shí)候就必須賦值,相當(dāng)于常量捌臊,可以避免線程安全問(wèn)題杨蛋,但是運(yùn)用的場(chǎng)景有限,一旦賦值不允許修改。
java重排序:
編譯期:編譯器在編譯的時(shí)候可能會(huì)打亂沒(méi)有依賴關(guān)系的代碼的編譯順序
CPU執(zhí)行指令:執(zhí)行不能保證編譯的順序逞力,且有時(shí)候會(huì)并發(fā)的執(zhí)行
寫(xiě)入主內(nèi)存時(shí)期:不能保證寄存器寫(xiě)會(huì)主內(nèi)存的順序是按照代碼寫(xiě)的那樣
證明java的重排序
注意只有當(dāng)多個(gè)線程的時(shí)候才會(huì)出現(xiàn)這種情況曙寡,單線程一定不會(huì)出現(xiàn)這種情況,因?yàn)榇嬖诰€程切換的時(shí)候寇荧,寄存器未寫(xiě)入到主內(nèi)存當(dāng)中举庶,才會(huì)這樣。
循環(huán)輸出的話揩抡,會(huì)出現(xiàn) a:1,x:0,b:1),y:0) 的情況的出現(xiàn)户侥,可以證明。
因?yàn)椴淮嬖谥嘏判虻脑挘?/p>
- 如果先執(zhí)行one線程峦嗤,則a=1蕊唐,y一定等于1;
- 如果先執(zhí)行other線程的話烁设,則b=1替梨,導(dǎo)致x一定等于1
- 所以不存在 x和y同時(shí)為0的情況發(fā)生
除非重排序,先執(zhí)行x=b=0的時(shí)候装黑,再切換線程y=a=0副瀑,的時(shí)候才會(huì)出現(xiàn)這種情況
public class Test {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
while(true){
x=y=a=b=0;
test();
}
}
public static void test() throws InterruptedException{
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
if(x+y==0){
System.out.println("a:" + a +",x:" + x + ",b:" + b +")"+ ",y:" + y +")");
}
}
}