(一)前言
學(xué)習(xí)多線程,要理解java內(nèi)存模型贱案,才能理解多線程情況下肛炮,數(shù)據(jù)的變化止吐,指令的運(yùn)行等宝踪,才能更好的了解多線程的運(yùn)行情況和日常使用的注意點(diǎn)。
(二)JMM與硬件內(nèi)存結(jié)構(gòu)
如上圖所示碍扔,可以看到JMM的大概結(jié)構(gòu)與硬件內(nèi)存結(jié)構(gòu)之間的關(guān)系瘩燥,每個(gè)線程只能訪問自己工作內(nèi)存的數(shù)據(jù),工作內(nèi)存中存儲(chǔ)著主內(nèi)存中變量復(fù)制的副本不同,這兩個(gè)內(nèi)存的數(shù)據(jù)可以存儲(chǔ)在硬件內(nèi)存中的任一地方厉膀,并沒有特殊劃分。
JMM只是一種抽象的概念二拐,是一種規(guī)則服鹅,并不真實(shí)存在,對(duì)于計(jì)算機(jī)而言百新,并不劃分工作內(nèi)存和主內(nèi)存企软,而是都存儲(chǔ)在計(jì)算機(jī)主內(nèi)存中。
(三)JMM的三種特性
1.原子性
在多線程環(huán)境下饭望,一個(gè)操作一旦開始就不會(huì)被其他線程影響仗哨。
比如一個(gè)靜態(tài)變量,被兩個(gè)線程同時(shí)進(jìn)行操作铅辞,無論如何運(yùn)行厌漂,最后的結(jié)構(gòu)必定是兩個(gè)線程中的一種結(jié)果。
特例:32位的系統(tǒng)斟珊,如果操作long或者double苇倡,由于操作位數(shù)問題,最終的結(jié)果可能并不是兩個(gè)線程中的任一結(jié)果。
其實(shí)旨椒,在上述描述中胜嗓,有一點(diǎn)無論如何運(yùn)行
,在計(jì)算機(jī)執(zhí)行程序中钩乍,為了提高性能辞州,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排。
指令重排
- (1)編譯器重排
簡(jiǎn)單的舉個(gè)例子:
主線程:
d=3寥粹;
c=3变过;
線程A:
a=c;
d=1;
線程B:
b=d;
c=2;
在以上兩個(gè)線程之前,對(duì)c和d進(jìn)行賦值涝涤,從程序的執(zhí)行順序來說媚狰,似乎不可能存在a=2,b=1
的情況,但是指令重排之后阔拳,可能存在:
線程A:
d=1;
a=c;
線程B:
c=2;
b=d;
此時(shí)崭孤,看起來就更可能存在a=2,b=1
的情況,所以糊肠,多線程情況下辨宠,對(duì)變量能否保持一致是不可預(yù)知的。
- (2)處理器重排
簡(jiǎn)單舉個(gè)例子:
a=b+c;
d=a-e
在上述代碼里面货裹,落實(shí)到指令可以理解為:
- 1.把b的值加載到寄存器
- 2.把c的值加載到寄存器
- 3.將b和c相加得到a
- 4.將a加載到寄存器
- 5.把e的值加載到寄存器
- 6.將a減e得到d
- 7.將d加載到寄存器嗤形。
其實(shí)上面的指令有個(gè)優(yōu)化的點(diǎn),就是將步驟5提前到2之后弧圆,因?yàn)椴襟E3和4都需要前面數(shù)據(jù)準(zhǔn)備好之后才能進(jìn)行赋兵,所以會(huì)進(jìn)行中斷,此時(shí)中斷搔预,會(huì)影響5的運(yùn)行霹期,將5提前,可以提高CPU的性能拯田。
重排保證了串行語(yǔ)義的執(zhí)行历造,但是在多線程的環(huán)境下,這樣是毀滅性的勿锅,導(dǎo)致結(jié)果的不可預(yù)知性帕膜。
如下代碼:
class MixedOrder{
int a = 0;
boolean flag = false;
public void writer(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
在單線程的場(chǎng)景下溢十,先調(diào)用writer()
垮刹,再次調(diào)用read()
,得到的結(jié)果是i=2
张弛。
在多線程的場(chǎng)景下荒典,指令重排之后酪劫,read()
方法在讀到flag
為true
的情況下,可能誤讀a=0
寺董,此時(shí)得到的結(jié)果為i=1
覆糟。
2.有序性
有序性是指在單線程的執(zhí)行代碼,我們可以認(rèn)為代碼的執(zhí)行是按照順序執(zhí)行的遮咖,但是在多線程場(chǎng)景下滩字,因?yàn)橹噶钪嘏牛瑢?dǎo)致最終的指令可能是亂序的御吞,在本線程內(nèi)麦箍,所有操作都視為有序的,但是多線程下陶珠,存在共享變量挟裂,一個(gè)線程需要觀察另一個(gè)線程,所以操作都是無序的揍诽。
3.可見性
可見性指的是當(dāng)一個(gè)線程修改了某個(gè)共享變量的值诀蓉,其他線程是否能夠馬上得知這個(gè)修改的值。這個(gè)概念僅代表在并發(fā)程序上的概念暑脆。由于每個(gè)線程會(huì)將共享變量拷貝到自己的工作線程中渠啤,由于指令重排的情況,也會(huì)存在可見性的問題饵筑,導(dǎo)致結(jié)果不是預(yù)期的結(jié)果埃篓。
(四)JMM提供的解決方案
針對(duì)以上的三種特性在多線程環(huán)境下的問題,JMM提供了相應(yīng)的解決方案根资。
- 原子性問題
除了JVM自身提供的對(duì)基本數(shù)據(jù)類型讀寫操作的原子性外,對(duì)于方法級(jí)別或者代碼塊級(jí)別的原子性操作同窘,可以使用synchronized
關(guān)鍵字或者重入鎖(ReentrantLock)
保證程序執(zhí)行的原子性玄帕。 - 可見性問題
可見性問題,可以使用synchronized
關(guān)鍵字或者volatile
關(guān)鍵字解決想邦,它們都可以使一個(gè)線程修改后的變量立即對(duì)其他線程可見裤纹。 - 有序性問題
對(duì)于指令重排導(dǎo)致的可見性問題和有序性問題,則可以利用volatile關(guān)鍵字解決丧没,因?yàn)関olatile的另外一個(gè)作用就是禁止重排序優(yōu)化鹰椒,關(guān)于volatile稍后會(huì)進(jìn)一步分析。
同時(shí)呕童,JMM內(nèi)部還定義一套happens-before 原則來保證多線程環(huán)境下兩個(gè)操作間的原子性漆际、可見性以及有序性。
happens-before 原則
- 1.程序順序原則
即在一個(gè)線程內(nèi)必須保證語(yǔ)義串行性夺饲,也就是說按照代碼順序執(zhí)行奸汇。 - 2.鎖規(guī)則
解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前施符,也就是說,如果對(duì)于一個(gè)鎖解鎖后擂找,再加鎖戳吝,那么加鎖的動(dòng)作必須在解鎖動(dòng)作之后(同一個(gè)鎖)。 - 3.volatile規(guī)則
volatile變量的寫贯涎,先發(fā)生于讀听哭,這保證了volatile變量的可見性,簡(jiǎn)單的理解就是塘雳,volatile變量在每次被線程訪問時(shí)欢唾,都強(qiáng)迫從主內(nèi)存中讀該變量的值,而當(dāng)該變量發(fā)生變化時(shí)粉捻,又會(huì)強(qiáng)迫將最新的值刷新到主內(nèi)存礁遣,任何時(shí)刻,不同的線程總是能夠看到該變量的最新值肩刃。 - 4.線程啟動(dòng)規(guī)則
線程的start()方法先于它的每一個(gè)動(dòng)作祟霍,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時(shí)盈包,線程A對(duì)共享變量的修改對(duì)線程B可見沸呐。 - 5.傳遞性
A先于B ,B先于C 那么A必然先于C - 6.線程終止規(guī)則
線程的所有操作先于線程的終結(jié)呢燥,Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止崭添。假設(shè)在線程B終止之前,修改了共享變量叛氨,線程A從線程B的join方法成功返回后呼渣,線程B對(duì)共享變量的修改將對(duì)線程A可見。 - 7.線程中斷規(guī)則
對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生寞埠,可以通過Thread.interrupted()方法檢測(cè)線程是否中斷屁置。 - 8.對(duì)象終結(jié)規(guī)則
對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法
(五)volatile
volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制仁连。volatile關(guān)鍵字有如下兩個(gè)作用:
- 保證被volatile修飾的共享變量對(duì)所有線程總數(shù)可見的蓝角,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值,新值總數(shù)可以被其他線程立即得知饭冬。
- 禁止指令重排序優(yōu)化使鹅。
volatile的可見性
關(guān)于volatile的可見性作用,我們必須意識(shí)到被volatile修飾的變量對(duì)所有線程總數(shù)立即可見的昌抠,對(duì)volatile變量的所有寫操作總是能立刻反應(yīng)到其他線程中患朱,但是對(duì)于volatile變量運(yùn)算操作在多線程環(huán)境并不保證安全性。
volatile禁止重排優(yōu)化
禁止重排其實(shí)在單例模式中已經(jīng)有提現(xiàn)扰魂,就是單例模式中的雙重校驗(yàn)鎖模式麦乞。
instance = new Singleton();
偽代碼如下:
memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance(memory); //2.初始化對(duì)象
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址蕴茴,此時(shí)instance!=null
如果去掉volatile姐直,則可重排優(yōu)化為:
memory = allocate(); //1.分配對(duì)象內(nèi)存空間
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址倦淀,此時(shí)instance!=null声畏,但是對(duì)象還沒有初始化完成撞叽!
instance(memory); //2.初始化對(duì)象
以上可以發(fā)現(xiàn),當(dāng)一條線程訪問instance不為null時(shí)插龄,由于instance實(shí)例未必已初始化完成愿棋,也就造成了線程安全問題。