Java互聯(lián)網(wǎng)架構(gòu)-并發(fā)編程底層原理分析

計(jì)算機(jī)中為什么會(huì)出現(xiàn)線程不安全的問(wèn)題

volatile既然是與線程安全有關(guān)的問(wèn)題蜗搔,那我們先來(lái)了解一下計(jì)算機(jī)在處理數(shù)據(jù)的過(guò)程中為什么會(huì)出現(xiàn)線程不安全的問(wèn)題八堡。

大家都知道,計(jì)算機(jī)在執(zhí)行程序時(shí)缝龄,每條指令都是在CPU中執(zhí)行的溶耘,而執(zhí)行指令過(guò)程中會(huì)涉及到數(shù)據(jù)的讀取和寫(xiě)入。由于程序運(yùn)行過(guò)程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的百新,這時(shí)就存在一個(gè)問(wèn)題庐扫,由于CPU執(zhí)行速度很快仗哨,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫(xiě)入數(shù)據(jù)的過(guò)程跟CPU執(zhí)行指令的速度比起來(lái)要慢的多厌漂,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過(guò)和內(nèi)存的交互來(lái)進(jìn)行斟珊,會(huì)大大降低指令執(zhí)行的速度。

為了處理這個(gè)問(wèn)題旨椒,在CPU里面就有了高速緩存(Cache)的概念堵漱。當(dāng)程序在運(yùn)行過(guò)程中,會(huì)將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中示惊,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫(xiě)入數(shù)據(jù)愉镰,當(dāng)運(yùn)算結(jié)束之后岛杀,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。

我舉個(gè)簡(jiǎn)單的例子类嗤,比如cpu在執(zhí)行下面這段代碼的時(shí)候,

t = t + 1;

會(huì)先從高速緩存中查看是否有t的值遗锣,如果有,則直接拿來(lái)使用弧圆,如果沒(méi)有笔咽,則會(huì)從主存中讀取,讀取之后會(huì)復(fù)制一份存放在高速緩存中方便下次使用拯田。之后cup進(jìn)行對(duì)t加1操作甩十,然后把數(shù)據(jù)寫(xiě)入高速緩存吭产,最后會(huì)把高速緩存中的數(shù)據(jù)刷新到主存中臣淤。

這一過(guò)程在單線程運(yùn)行是沒(méi)有問(wèn)題的窃爷,但是在多線程中運(yùn)行就會(huì)有問(wèn)題了。在多核CPU中寺董,每條線程可能運(yùn)行于不同的CPU中刻剥,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對(duì)單核CPU來(lái)說(shuō)滩字,其實(shí)也會(huì)出現(xiàn)這種問(wèn)題麦箍,只不過(guò)是以線程調(diào)度的形式來(lái)分別執(zhí)行的,本次講解以多核cup為主)享钞。這時(shí)就會(huì)出現(xiàn)同一個(gè)變量在兩個(gè)高速緩存中的不一致問(wèn)題了诀蓉。

例如:

兩個(gè)線程分別讀取了t的值,假設(shè)此時(shí)t的值為0狐肢,并且把t的值存到了各自的高速緩存中沥曹,然后線程1對(duì)t進(jìn)行了加1操作,此時(shí)t的值為1僵腺,并且把t的值寫(xiě)回到主存中壶栋。但是線程2中高速緩存的值還是0委刘,進(jìn)行加1操作之后鹰椒,t的值還是為1呕童,然后再把t的值寫(xiě)回主存。

此時(shí)奸汇,就出現(xiàn)了線程不安全問(wèn)題了往声。

Java中的線程安全問(wèn)題

上面那種線程安全問(wèn)題浩销,可能對(duì)于不同的操作系統(tǒng)會(huì)有不同的處理機(jī)制,例如Windows操作系統(tǒng)和Linux的操作系統(tǒng)的處理方法可能會(huì)不同塘雳。

我們都知道普筹,Java是一種夸平臺(tái)的語(yǔ)言,因此Java這種語(yǔ)言在處理線程安全問(wèn)題的時(shí)候妻顶,會(huì)有自己的處理機(jī)制蜒车,例如volatile關(guān)鍵字,synchronized關(guān)鍵字呢燥,并且這種機(jī)制適用于各種平臺(tái)寓娩。

Java內(nèi)存模型規(guī)定所有的變量都是存在主存當(dāng)中(類(lèi)似于前面說(shuō)的物理內(nèi)存)棘伴,每個(gè)線程都有自己的工作內(nèi)存(類(lèi)似于前面的高速緩存)。線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行仁连,而不能直接對(duì)主存進(jìn)行操作。并且每個(gè)線程不能訪問(wèn)其他線程的工作內(nèi)存使鹅。

由于java中的每個(gè)線程有自己的工作空間昌抠,這種工作空間相當(dāng)于上面所說(shuō)的高速緩存,因此多個(gè)線程在處理一個(gè)共享變量的時(shí)候裁厅,就會(huì)出現(xiàn)線程安全問(wèn)題侨艾。

這里簡(jiǎn)單解釋下共享變量唠梨,上面我們所說(shuō)的t就是一個(gè)共享變量,也就是說(shuō)插龄,能夠被多個(gè)線程訪問(wèn)到的變量科展,我們稱(chēng)之為共享變量糠雨。在java中共享變量包括實(shí)例變量甘邀,靜態(tài)變量,數(shù)組元素坞琴。他們都被存放在堆內(nèi)存中逗抑。

volatile關(guān)鍵字

上面扯了一大堆,都沒(méi)提到volatile關(guān)鍵字的作用荧关,下面開(kāi)始講解volatile關(guān)鍵字是如何保證線程安全問(wèn)題的褂傀。

可見(jiàn)性

什么是可見(jiàn)性仙辟?

意思就是說(shuō)鳄梅,在多線程環(huán)境下未檩,某個(gè)共享變量如果被其中一個(gè)線程給修改了讹挎,其他線程能夠立即知道這個(gè)共享變量已經(jīng)被修改了,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候马篮,最終會(huì)去內(nèi)存中讀取怜奖,而不是從自己的工作空間中讀取。

例如我們上面說(shuō)的迁央,當(dāng)線程1對(duì)t進(jìn)行了加1操作并把數(shù)據(jù)寫(xiě)回到主存之后滥崩,線程2就會(huì)知道它自己工作空間內(nèi)的t已經(jīng)被修改了钙皮,當(dāng)它要執(zhí)行加1操作之后,就會(huì)去主存中讀取导匣。這樣茸时,兩邊的數(shù)據(jù)就能一致了。

假如一個(gè)變量被聲明為volatile缓待,那么這個(gè)變量就具有了可見(jiàn)性的性質(zhì)了命斧。這就是volatile關(guān)鍵的作用之一了嘱兼。

volatile保證變量可見(jiàn)性的原理

當(dāng)一個(gè)變量被聲明為volatile時(shí),在編譯成會(huì)變指令的時(shí)候汇四,會(huì)多出下面一行:

0x00bbacde: lock add1 $0x0,(%esp);

這句指令的意思就是在寄存器執(zhí)行一個(gè)加0的空操作。不過(guò)這條指令的前面有一個(gè)lock(鎖)前綴序宦。

當(dāng)處理器在處理?yè)碛衛(wèi)ock前綴的指令時(shí):

在之前的處理中互捌,lock會(huì)導(dǎo)致傳輸數(shù)據(jù)的總線被鎖定行剂,其他處理器都不能訪問(wèn)總線,從而保證處理lock指令的處理器能夠獨(dú)享操作數(shù)據(jù)所在的內(nèi)存區(qū)域腌巾,而不會(huì)被其他處理所干擾铲觉。

但由于總線被鎖住撵幽,其他處理器都會(huì)被堵住,從而影響了多處理器的執(zhí)行效率漏麦。為了解決這個(gè)問(wèn)題况褪,在后來(lái)的處理器中测垛,處理器遇到lock指令時(shí)不會(huì)再鎖住總線秧均,而是會(huì)檢查數(shù)據(jù)所在的內(nèi)存區(qū)域,如果該數(shù)據(jù)是在處理器的內(nèi)部緩存中锯七,則會(huì)鎖定此緩存區(qū)域誉己,處理完后把緩存寫(xiě)回到主存中,并且會(huì)利用緩存一致性協(xié)議來(lái)保證其他處理器中的緩存數(shù)據(jù)的一致性霉祸。

緩存一致性協(xié)議

剛才我在說(shuō)可見(jiàn)性的時(shí)候袱蜡,說(shuō)“如果一個(gè)共享變量被一個(gè)線程修改了之后,當(dāng)其他線程要讀取這個(gè)變量的時(shí)候奔穿,最終會(huì)去內(nèi)存中讀取贱田,而不是從自己的工作空間中讀取”茵典,實(shí)際上是這樣的:

線程中的處理器會(huì)一直在總線上嗅探其內(nèi)部緩存中的內(nèi)存地址在其他處理器的操作情況,一旦嗅探到某處處理器打算修改其內(nèi)存地址中的值彩倚,而該內(nèi)存地址剛好也在自己的內(nèi)部緩存中帆离,那么處理器就會(huì)強(qiáng)制讓自己對(duì)該緩存地址的無(wú)效结澄。所以當(dāng)該處理器要訪問(wèn)該數(shù)據(jù)的時(shí)候麻献,由于發(fā)現(xiàn)自己緩存的數(shù)據(jù)無(wú)效了,就會(huì)去主存中訪問(wèn)监婶。

有序性

實(shí)際上齿桃,當(dāng)我們把代碼寫(xiě)好之后,虛擬機(jī)不一定會(huì)按照我們寫(xiě)的代碼的順序來(lái)執(zhí)行带污。例如對(duì)于下面的兩句代碼:

int a = 1;

int b = 2;

對(duì)于這兩句代碼鱼冀,你會(huì)發(fā)現(xiàn)無(wú)論是先執(zhí)行a = 1還是執(zhí)行b = 2,都不會(huì)對(duì)a,b最終的值造成影響泛烙。所以虛擬機(jī)在編譯的時(shí)候翘紊,是有可能把他們進(jìn)行重排序的帆疟。

為什么要進(jìn)行重排序呢?

你想啊自赔,假如執(zhí)行 int a = 1這句代碼需要100ms的時(shí)間柳琢,但執(zhí)行int b = 2這句代碼需要1ms的時(shí)間绍妨,并且先執(zhí)行哪句代碼并不會(huì)對(duì)a,b最終的值造成影響。那當(dāng)然是先執(zhí)行int b = 2這句代碼了柬脸。

所以他去,虛擬機(jī)在進(jìn)行代碼編譯優(yōu)化的時(shí)候,對(duì)于那些改變順序之后不會(huì)對(duì)最終變量的值造成影響的代碼倒堕,是有可能將他們進(jìn)行重排序的灾测。

更多代碼編譯優(yōu)化可以看我寫(xiě)的另一篇文章:

虛擬機(jī)在運(yùn)行期對(duì)代碼的優(yōu)化策略

那么重排序之后真的不會(huì)對(duì)代碼造成影響嗎?

實(shí)際上垦巴,對(duì)于有些代碼進(jìn)行重排序之后媳搪,雖然對(duì)變量的值沒(méi)有造成影響,但有可能會(huì)出現(xiàn)線程安全問(wèn)題的秦爆。具體請(qǐng)看下面的代碼

public class NoVisibility{

private static boolean ready;

private static int number;

private static class Reader extends Thread{

public void run(){

while(!ready){

Thread.yield();

}

System.out.println(number);

}

}

public static void main(String[] args){

new Reader().start();

number = 42;

ready = true;

}

}

這段代碼最終打印的一定是42嗎?如果沒(méi)有重排序的話憔披,打印的確實(shí)會(huì)是42鲜结,但如果number = 42和ready = true被進(jìn)行了重排序,顛倒了順序活逆,那么就有可能打印出0了,而不是42拗胜。(因?yàn)閚umber的初始值會(huì)是0).

因此蔗候,重排序是有可能導(dǎo)致線程安全問(wèn)題的。

如果一個(gè)變量被聲明volatile的話埂软,那么這個(gè)變量不會(huì)被進(jìn)行重排序锈遥,也就是說(shuō)纫事,虛擬機(jī)會(huì)保證這個(gè)變量之前的代碼一定會(huì)比它先執(zhí)行,而之后的代碼一定會(huì)比它慢執(zhí)行所灸。

例如把上面中的number聲明為volatile丽惶,那么number = 42一定會(huì)比ready = true先執(zhí)行。

不過(guò)這里需要注意的是爬立,虛擬機(jī)只是保證這個(gè)變量之前的代碼一定比它先執(zhí)行钾唬,但并沒(méi)有保證這個(gè)變量之前的代碼不可以重排序。之后的也一樣侠驯。

volatile關(guān)鍵字能夠保證代碼的有序性抡秆,這個(gè)也是volatile關(guān)鍵字的作用。

總結(jié)一下吟策,一個(gè)被volatile聲明的變量主要有以下兩種特性保證保證線程安全儒士。

可見(jiàn)性。

有序性檩坚。

volatile真的能完全保證一個(gè)變量的線程安全嗎着撩?

我們通過(guò)上面的講解,發(fā)現(xiàn)volatile關(guān)鍵字還是挺有用的匾委,不但能夠保證變量的可見(jiàn)性拖叙,還能保證代碼的有序性。

那么剩檀,它真的能夠保證一個(gè)變量在多線程環(huán)境下都能被正確的使用嗎憋沿?

答案是否定的。原因是因?yàn)镴ava里面的運(yùn)算并非是原子操作沪猴。

原子操作

原子操作:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷辐啄,要么就都不執(zhí)行。

也就是說(shuō)运嗜,處理器要嘛把這組操作全部執(zhí)行完壶辜,中間不允許被其他操作所打斷,要嘛這組操作不要執(zhí)行担租。

剛才說(shuō)Java里面的運(yùn)行并非是原子操作砸民。我舉個(gè)例子,例如這句代碼

int a = b + 1;

處理器在處理代碼的時(shí)候奋救,需要處理以下三個(gè)操作:

從內(nèi)存中讀取b的值岭参。

進(jìn)行a = b + 1這個(gè)運(yùn)算

把a(bǔ)的值寫(xiě)回到內(nèi)存中

而這三個(gè)操作處理器是不一定就會(huì)連續(xù)執(zhí)行的,有可能執(zhí)行了第一個(gè)操作之后尝艘,處理器就跑去執(zhí)行別的操作的演侯。

證明volatile無(wú)法保證線程安全的例子

由于Java中的運(yùn)算并非是原子操作,所以導(dǎo)致volatile聲明的變量無(wú)法保證線程安全背亥。

對(duì)于這句話秒际,我給大家舉個(gè)例子悬赏。代碼如下:

public class Test{

public static volatile int t = 0;

public static void main(String[] args){

Thread[] threads = new Thread[10];

for(int i = 0; i < 10; i++){

//每個(gè)線程對(duì)t進(jìn)行1000次加1的操作

threads[i] new Thread(new Runnable(){

@Override

public void run(){

for(int j = 0; j < 1000; j++){

t = t + 1;

}

}

});

threads[i].start();

}

//等待所有累加線程都結(jié)束

while(Thread.activeCount() > 1){

Thread.yield();

}

//打印t的值

System.out.println(t);

}

}

最終的打印結(jié)果會(huì)是1000 * 10 = 10000嗎?答案是否定的娄徊。

問(wèn)題就出現(xiàn)在t = t + 1這句代碼中闽颇。我們來(lái)分析一下

例如:

線程1讀取了t的值,假如t = 0寄锐。之后線程2讀取了t的值兵多,此時(shí)t = 0。

然后線程1執(zhí)行了加1的操作锐峭,此時(shí)t = 1中鼠。但是這個(gè)時(shí)候,處理器還沒(méi)有把t = 1的值寫(xiě)回主存中沿癞。這個(gè)時(shí)候處理器跑去執(zhí)行線程2援雇,注意,剛才線程2已經(jīng)讀取了t的值椎扬,所以這個(gè)時(shí)候并不會(huì)再去讀取t的值了惫搏,所以此時(shí)t的值還是0,然后線程2執(zhí)行了對(duì)t的加1操作蚕涤,此時(shí)t =1 筐赔。

這個(gè)時(shí)候,就出現(xiàn)了線程安全問(wèn)題了揖铜,兩個(gè)線程都對(duì)t執(zhí)行了加1操作茴丰,但t的值卻是1。所以說(shuō)天吓,volatile關(guān)鍵字并不一定能夠保證變量的安全性贿肩。

什么情況下volatile能夠保證線程安全

剛才雖然說(shuō),volatile關(guān)鍵字不一定能夠保證線程安全的問(wèn)題龄寞,其實(shí)汰规,在大多數(shù)情況下volatile還是可以保證變量的線程安全問(wèn)題的。所以物邑,在滿(mǎn)足以下兩個(gè)條件的情況下溜哮,volatile就能保證變量的線程安全問(wèn)題:

運(yùn)算結(jié)果并不依賴(lài)變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值色解。

變量不需要與其他狀態(tài)變量共同參與不變約束茂嗓。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市科阎,隨后出現(xiàn)的幾起案子在抛,更是在濱河造成了極大的恐慌,老刑警劉巖萧恕,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刚梭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡票唆,警方通過(guò)查閱死者的電腦和手機(jī)朴读,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)走趋,“玉大人衅金,你說(shuō)我怎么就攤上這事〔净停” “怎么了氮唯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)姨伟。 經(jīng)常有香客問(wèn)我惩琉,道長(zhǎng),這世上最難降的妖魔是什么夺荒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任瞒渠,我火速辦了婚禮,結(jié)果婚禮上技扼,老公的妹妹穿的比我還像新娘伍玖。我一直安慰自己,他們只是感情好剿吻,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布窍箍。 她就那樣靜靜地躺著,像睡著了一般丽旅。 火紅的嫁衣襯著肌膚如雪椰棘。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天魔招,我揣著相機(jī)與錄音晰搀,去河邊找鬼。 笑死办斑,一個(gè)胖子當(dāng)著我的面吹牛外恕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乡翅,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鳞疲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蠕蚜?” 一聲冷哼從身側(cè)響起尚洽,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎靶累,沒(méi)想到半個(gè)月后腺毫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體癣疟,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年潮酒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了睛挚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡急黎,死狀恐怖扎狱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勃教,我是刑警寧澤淤击,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布污抬,位于F島的核電站,受9級(jí)特大地震影響心软,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜删铃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一耳贬、第九天 我趴在偏房一處隱蔽的房頂上張望猎唁。 院中可真熱鬧咒劲,春花似錦、人聲如沸诫隅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)逐纬。三九已至蛔屹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豁生,已是汗流浹背兔毒。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甸箱,地道東北人育叁。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像芍殖,于是被迫代替她去往敵國(guó)和親豪嗽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 事如舟掛短篷,或移西岸或移東龟梦。 幾回缺月還圓月隐锭,數(shù)陣南風(fēng)又北風(fēng)。 歲久人無(wú)千日好变秦,春深花有幾時(shí)紅成榜。 是非入耳君須忍...
    Anita_謝玉婷閱讀 689評(píng)論 0 0
  • 要讓寶寶從兩三歲起就知道調(diào)皮的界限,跨了界刘绣,父母當(dāng)然會(huì)生氣樱溉。但,生氣是一回事兒纬凤,講道理是另外一回事福贞,并不沖突。 孩...
    瓜爾佳小鑫閱讀 312評(píng)論 0 3