摘要:所以這情況下,當(dāng)線程操作變量的時(shí)候,變量并不對(duì)線程可見(jiàn)。總結(jié),緩存引發(fā)的可見(jiàn)性問(wèn)題,切換線程帶來(lái)的原子性問(wèn)題,編譯帶來(lái)的有序性問(wèn)題深刻理解這些前因后果,可以診斷大部分并發(fā)的問(wèn)題
背景介紹
如何解決并發(fā)問(wèn)題,首先要理解并發(fā)問(wèn)題的實(shí)際源頭怎么發(fā)生的。
現(xiàn)代計(jì)算機(jī)的不同硬件的運(yùn)行速度是差異很大的,這個(gè)大家應(yīng)該都是知道的。
計(jì)算機(jī)數(shù)據(jù)傳輸運(yùn)行速度上的快慢比較:
CPU > 緩存 > I/O
如何最大化的讓不同速度的硬件可以更好的協(xié)調(diào)執(zhí)行,需要做一些“撮合”的工作
CUP增加了高速緩存來(lái)均衡與緩存間的速度差異
操作系統(tǒng)增加了 進(jìn)程,線程,以分時(shí)復(fù)用CPU,進(jìn)而均衡CPU與I/O的速度差異(當(dāng)?shù)却齀/O的時(shí)候系統(tǒng)切換CPU給系統(tǒng)程序使用)
現(xiàn)代編程語(yǔ)言的編譯器優(yōu)化指令順序,使得緩存能夠合理的利用
上面說(shuō)來(lái)并發(fā)才生問(wèn)題的背景,下面說(shuō)下并發(fā)產(chǎn)生的具體原因是什么
并發(fā)產(chǎn)生的原因 緩存導(dǎo)致的可見(jiàn)性問(wèn)題先看下單核CPU和緩存之間的關(guān)系:
單核情況下,也是最簡(jiǎn)單的情況,線程A操作寫(xiě)入變量A,這個(gè)變量A的值肯定是被線程B所見(jiàn)的。因?yàn)?個(gè)線程是在一個(gè)CPU上操作,所用的也是同一個(gè)CPU緩存。
這里我們來(lái)定義
一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到,我們稱為 “可見(jiàn)性”
多核CPU時(shí)代下,我們?cè)趤?lái)看下具體情況:
很明顯,多核情況下每個(gè)CPU都有自己的高速緩存,所以變量A的在每個(gè)CPU中可能是不同步的,不一致的。
結(jié)果程A剛好操作來(lái)CPU1的緩存,而線程B也剛好只操作了CPU2的緩存。所以這情況下,當(dāng)線程A操作變量A的時(shí)候,變量并不對(duì)線程B可見(jiàn)。
我們用一段經(jīng)典的代碼說(shuō)明下可見(jiàn)性的問(wèn)題:
private void add10K() { int idx = 0; while (idx++ < 100000) { count += 1; } } @Test public void demo() { // 創(chuàng)建兩個(gè)線程,執(zhí)行 add() 操作 Thread th1 = new Thread(() -> { add10K(); }); Thread th2 = new Thread(() -> { add10K(); }); // 啟動(dòng)兩個(gè)線程 th1.start(); th2.start(); // 等待兩個(gè)線程執(zhí)行結(jié)束 try { th1.join(); th2.join(); } catch (Exception exc) { exc.printStackTrace(); } System.out.println(count); }
大家應(yīng)該都知道,答案肯定不是 200000
這就是可見(jiàn)性導(dǎo)致的問(wèn)題,因?yàn)?個(gè)線程讀取變量count時(shí),讀取的都是自己CPU下的高速緩存內(nèi)的緩存值,+1時(shí)也是在自己的高速緩存中。
進(jìn)程切換最早是為了提高CPU的使用率而出現(xiàn)的。
比如,50毫米操作系統(tǒng)會(huì)重新選擇一個(gè)進(jìn)程來(lái)執(zhí)行(任務(wù)切換),50毫米成為“時(shí)間片”
早期的操作系統(tǒng)是進(jìn)程間的切換,進(jìn)程間的內(nèi)存空間是不共享的,切換需要切換內(nèi)存映射地址,切換成本大。
而一個(gè)進(jìn)程創(chuàng)建的所有線程,內(nèi)存空間都是共享的。所以現(xiàn)在的操作系統(tǒng)都是基于更輕量的線程實(shí)現(xiàn)切換的,現(xiàn)在我們提到的“任務(wù)切換”都是線程切換。
任務(wù)切換的時(shí)機(jī)大多數(shù)在“時(shí)間片”結(jié)束的時(shí)候。
現(xiàn)在我們使用的基本都是高級(jí)語(yǔ)言,高級(jí)語(yǔ)言的一句對(duì)應(yīng)多條CPU命令,比如 count +=1 至少對(duì)應(yīng)3條CPU命令,指令:
1, 從內(nèi)存加載到CPU的寄存器
2, 在寄存器執(zhí)行 +1
3, 最后,講結(jié)果寫(xiě)回內(nèi)存(緩存機(jī)制導(dǎo)致可能寫(xiě)入的是CPU緩存而不是內(nèi)存)
操作系統(tǒng)做任務(wù)切換,會(huì)在 任意一條CPU指令執(zhí)行完就行切換。所以會(huì)導(dǎo)致問(wèn)題
如圖所示,線程A當(dāng)執(zhí)行完初始化count=0時(shí)候,剛好被線程切換給了線程B。線程B執(zhí)行count+1=1并最后寫(xiě)入值到緩存中,CPU切換回線程A后,繼續(xù)執(zhí)行A線程的count+1=1并再次寫(xiě)入緩存,最后緩存中的count還是為1.
一開(kāi)始我們?nèi)蝿?wù)count+1=1應(yīng)該是一個(gè)不能再被拆開(kāi)的原子操作。
我們把一個(gè)或多個(gè)操作在CPU執(zhí)行過(guò)程中的不被中斷的特性稱為 原子性。
CPU能夠保證的原子性,是CPU指令級(jí)別的。所以高級(jí)語(yǔ)言需要語(yǔ)言層面 保證操作的原子性。
編譯優(yōu)化帶來(lái)的有序性問(wèn)題有序性。顧名思義,有序性指的是程序按照代碼的先后順序執(zhí)行。
編譯器為了優(yōu)化性能,有時(shí)候會(huì)改變程序中語(yǔ)句的先后順序,例如程序中:a=6;b=7;編譯器優(yōu)化后可能變成b=7;a=6;,在這個(gè)例子中,編譯器調(diào)整了語(yǔ)句的順序,但是不影響程序的最終結(jié)果。不過(guò)有時(shí)候編譯器及解釋器的優(yōu)化可能導(dǎo)致意想不到的 Bug。
Java中的經(jīng)典案例,雙重檢查創(chuàng)建單例對(duì)象;
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
看似完美的代碼,其實(shí)有問(wèn)題。問(wèn)題就在new上。
想象中 new操作步驟:
1,分配一塊內(nèi)存 M
2,在內(nèi)存M上 初始化對(duì)象
3,把內(nèi)存M地址賦值給 變量
實(shí)際上就行編譯后的順序是:
1,分開(kāi)一塊內(nèi)存 M
2,把內(nèi)存M地址賦值給 變量
3,在 內(nèi)存M上 初始化對(duì)象
優(yōu)化導(dǎo)致的問(wèn)題:
如圖所示,當(dāng)線程A執(zhí)行到第二步的時(shí)候,被線程切換了,這時(shí)候,instance未初始化實(shí)例的對(duì)象,而線程B這時(shí)候執(zhí)行到instance == null ?的判斷中,發(fā)現(xiàn)instance已經(jīng)有“值”了,導(dǎo)致了返回了一個(gè)空對(duì)象的異常。
總結(jié)1,緩存引發(fā)的可見(jiàn)性問(wèn)題
2,切換線程帶來(lái)的原子性問(wèn)題
3,編譯帶來(lái)的有序性問(wèn)題
深刻理解這些前因后果,可以診斷大部分并發(fā)的問(wèn)題!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/73764.html
摘要:最后,總結(jié)一下,導(dǎo)致并發(fā)問(wèn)題的三個(gè)源頭分別是原子性一個(gè)線程在執(zhí)行的過(guò)程當(dāng)中不被中斷??梢?jiàn)性一個(gè)線程修改了共享變量,另一個(gè)線程能夠馬上看到,就叫做可見(jiàn)性。 計(jì)算機(jī)的 CPU、內(nèi)存、I/O 設(shè)備的速度一直存在較大的差異,依次是 CPU > 內(nèi)存 > I/O 設(shè)備,為了權(quán)衡這三者的速度差異,主要提出了三種解決辦法: CPU 增加了緩存,均衡和內(nèi)存的速度差異 發(fā)明了進(jìn)程、線程,分時(shí)復(fù)用 CP...
摘要:因?yàn)槎嗑€程競(jìng)爭(zhēng)鎖時(shí)會(huì)引起上下文切換。減少線程的使用。舉個(gè)例子如果說(shuō)服務(wù)器的帶寬只有,某個(gè)資源的下載速度是,系統(tǒng)啟動(dòng)個(gè)線程下載該資源并不會(huì)導(dǎo)致下載速度編程,所以在并發(fā)編程時(shí),需要考慮這些資源的限制。 最近私下做一項(xiàng)目,一bug幾日未解決,總惶恐。一日頓悟,bug不可怕,怕的是項(xiàng)目不存在bug,與其懼怕,何不與其剛正面。 系列文章傳送門(mén): Java多線程學(xué)習(xí)(一)Java多線程入門(mén) Jav...
摘要:此時(shí)線程和會(huì)再有一個(gè)線程能夠獲取寫(xiě)鎖,假設(shè)是,如果不采用再次驗(yàn)證的方式,此時(shí)會(huì)再次查詢數(shù)據(jù)庫(kù)。而實(shí)際上線程已經(jīng)把緩存的值設(shè)置好了,完全沒(méi)有必要再次查詢數(shù)據(jù)庫(kù)。 大家知道了Java中使用管程同步原語(yǔ),理論上可以解決所有的并發(fā)問(wèn)題。那 Java SDK 并發(fā)包里為什么還有很多其他的工具類(lèi)呢?原因很簡(jiǎn)單:分場(chǎng)景優(yōu)化性能,提升易用性 今天我們就介紹一種非常普遍的并發(fā)場(chǎng)景:讀多寫(xiě)少場(chǎng)景。實(shí)際工作...
閱讀 2646·2021-10-14 09:47
閱讀 4939·2021-09-22 15:52
閱讀 3361·2019-08-30 15:53
閱讀 1458·2019-08-30 15:44
閱讀 689·2019-08-29 16:41
閱讀 1657·2019-08-29 16:28
閱讀 448·2019-08-29 15:23
閱讀 1628·2019-08-26 12:20