這篇文章主要介紹“并發(fā)編程中我們需要注意的問(wèn)題有哪些”,在日常操作中,相信很多人在并發(fā)編程中我們需要注意的問(wèn)題有哪些問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”并發(fā)編程中我們需要注意的問(wèn)題有哪些”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
十年專(zhuān)注成都網(wǎng)站制作,企業(yè)網(wǎng)站設(shè)計(jì),個(gè)人網(wǎng)站制作服務(wù),為大家分享網(wǎng)站制作知識(shí)、方案,網(wǎng)站設(shè)計(jì)流程、步驟,成功服務(wù)上千家企業(yè)。為您提供網(wǎng)站建設(shè),網(wǎng)站制作,網(wǎng)頁(yè)設(shè)計(jì)及定制高端網(wǎng)站建設(shè)服務(wù),專(zhuān)注于企業(yè)網(wǎng)站設(shè)計(jì),高端網(wǎng)頁(yè)制作,對(duì)成都封陽(yáng)臺(tái)等多個(gè)方面,擁有多年的網(wǎng)站維護(hù)經(jīng)驗(yàn)。
相信你一定聽(tīng)說(shuō)過(guò)類(lèi)似這樣的描述:這個(gè)方法不是線(xiàn)程安全的,這個(gè)類(lèi)不是線(xiàn)程安全的,等等。
那什么是線(xiàn)程安全呢?其實(shí)本質(zhì)上就是正確性,而正確性的含義就是程序按照我們期望的執(zhí)行,不要讓我們感到意外。在上一篇《深入底層探究并發(fā)編程Bug罪魁禍?zhǔn)?mdash;—可見(jiàn)性、原子性、有序性 》中,我們已經(jīng)見(jiàn)識(shí)過(guò)很多詭異的 Bug,都是出乎我們預(yù)料的,它們都沒(méi)有按照我們期望的執(zhí)行。
那如何才能寫(xiě)出線(xiàn)程安全的程序呢?在上一篇中已經(jīng)介紹了并發(fā) Bug 的三個(gè)主要源頭:原子性問(wèn)題、可見(jiàn)性問(wèn)題和有序性問(wèn)題。也就是說(shuō),理論上線(xiàn)程安全的程序,就要避免出現(xiàn)原子性問(wèn)題、可見(jiàn)性問(wèn)題和有序性問(wèn)題。
那是不是所有的代碼都需要認(rèn)真分析一遍是否存在這三個(gè)問(wèn)題呢?當(dāng)然不是,其實(shí)只有一種情況需要:存在共享數(shù)據(jù)并且該數(shù)據(jù)會(huì)發(fā)生變化,通俗地講就是有多個(gè)線(xiàn)程會(huì)同時(shí)讀寫(xiě)同一數(shù)據(jù)。那如果能夠做到不共享數(shù)據(jù)或者數(shù)據(jù)狀態(tài)不發(fā)生變化,不就能夠保證線(xiàn)程的安全性了嘛。有不少技術(shù)方案都是基于這個(gè)理論的,例如線(xiàn)程本地存儲(chǔ)(Thread Local Storage,TLS)、不變模式等等,后面我會(huì)詳細(xì)介紹相關(guān)的技術(shù)方案是如何在 Java 語(yǔ)言中實(shí)現(xiàn)的。
但是,現(xiàn)實(shí)生活中,必須共享會(huì)發(fā)生變化的數(shù)據(jù),這樣的應(yīng)用場(chǎng)景還是很多的。
當(dāng)多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)同一數(shù)據(jù),并且至少有一個(gè)線(xiàn)程會(huì)寫(xiě)這個(gè)數(shù)據(jù)的時(shí)候,如果我們不采取防護(hù)措施,那么就會(huì)導(dǎo)致并發(fā) Bug,對(duì)此還有一個(gè)專(zhuān)業(yè)的術(shù)語(yǔ),叫做數(shù)據(jù)競(jìng)爭(zhēng)(DataRace)。比如,前面這篇文章里有個(gè) add10K() 的方法,當(dāng)多個(gè)線(xiàn)程調(diào)用時(shí)候就會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng),如下所示。
public class Test { private long count = 0; void add10K() { int idx = 0; while ( idx++ < 10000 ) { count += 1; } } }
那是不是在訪(fǎng)問(wèn)數(shù)據(jù)的地方,我們加個(gè)鎖保護(hù)一下就能解決所有的并發(fā)問(wèn)題了呢?顯然沒(méi)有這么簡(jiǎn)單。例如,對(duì)于上面示例,我們稍作修改,增加兩個(gè)被 synchronized 修飾的 get()和 set() 方法, add10K() 方法里面通過(guò) get() 和 set() 方法來(lái)訪(fǎng)問(wèn) value 變量,修改后的代碼如下所示。對(duì)于修改后的代碼,所有訪(fǎng)問(wèn)共享變量 value 的地方,我們都增加了互斥鎖,此時(shí)是不存在數(shù)據(jù)競(jìng)爭(zhēng)的。但很顯然修改后的 add10K() 方法并不是線(xiàn)程安全的。
public class Test { private long count = 0; synchronized long get() { return count ; 5 } synchronized void set( long v ) { count = v; } void add10K() { int idx = 0; while ( idx++ < 10000 ) { set( get() + 1 ) } } }
假設(shè) count=0,當(dāng)兩個(gè)線(xiàn)程同時(shí)執(zhí)行 get() 方法時(shí),get() 方法會(huì)返回相同的值 0,兩個(gè)線(xiàn)程執(zhí)行 get()+1 操作,結(jié)果都是 1,之后兩個(gè)線(xiàn)程再將結(jié)果 1 寫(xiě)入了內(nèi)存。你本來(lái)期望的是 2,而結(jié)果卻是 1。
這種問(wèn)題,有個(gè)官方的稱(chēng)呼,叫競(jìng)態(tài)條件(Race Condition)。所謂競(jìng)態(tài)條件,指的是程序的執(zhí)行結(jié)果依賴(lài)線(xiàn)程執(zhí)行的順序。例如上面的例子,如果兩個(gè)線(xiàn)程完全同時(shí)執(zhí)行,那么結(jié)果是 1;如果兩個(gè)線(xiàn)程是前后執(zhí)行,那么結(jié)果就是 2。在并發(fā)環(huán)境里,線(xiàn)程的執(zhí)行順序是不確定的,如果程序存在競(jìng)態(tài)條件問(wèn)題,那就意味著程序執(zhí)行的結(jié)果是不確定的,而執(zhí)行結(jié)果不確定這可是個(gè)大 Bug。
下面再結(jié)合一個(gè)例子來(lái)說(shuō)明下競(jìng)態(tài)條件,就是前面文章中提到的轉(zhuǎn)賬操作。轉(zhuǎn)賬操作里面有個(gè)判斷條件——轉(zhuǎn)出金額不能大于賬戶(hù)余額,但在并發(fā)環(huán)境里面,如果不加控制,當(dāng)多個(gè)線(xiàn)程同時(shí)對(duì)一個(gè)賬號(hào)執(zhí)行轉(zhuǎn)出操作時(shí),就有可能出現(xiàn)超額轉(zhuǎn)出問(wèn)題。假設(shè)賬戶(hù) A 有余額200,線(xiàn)程 1 和線(xiàn)程 2 都要從賬戶(hù) A 轉(zhuǎn)出 150,在下面的代碼里,有可能線(xiàn)程 1 和線(xiàn)程 2同時(shí)執(zhí)行到第 6 行,這樣線(xiàn)程 1 和線(xiàn)程 2 都會(huì)發(fā)現(xiàn)轉(zhuǎn)出金額 150 小于賬戶(hù)余額 200,于是就會(huì)發(fā)生超額轉(zhuǎn)出的情況。
class Account { private int balance; /* 轉(zhuǎn)賬 */ void transfer( Account target, int amt ) { if ( this.balance > amt ) { this.balance -= amt; target.balance += amt; } } }
所以你也可以按照下面這樣來(lái)理解競(jìng)態(tài)條件。在并發(fā)場(chǎng)景中,程序的執(zhí)行依賴(lài)于某個(gè)狀態(tài)變量,也就是類(lèi)似于下面這樣:
if (狀態(tài)變量 滿(mǎn)足 執(zhí)行條件) { 執(zhí)行操作 }
當(dāng)某個(gè)線(xiàn)程發(fā)現(xiàn)狀態(tài)變量滿(mǎn)足執(zhí)行條件后,開(kāi)始執(zhí)行操作;可是就在這個(gè)線(xiàn)程執(zhí)行操作的時(shí)候,其他線(xiàn)程同時(shí)修改了狀態(tài)變量,導(dǎo)致?tīng)顟B(tài)變量不滿(mǎn)足執(zhí)行條件了。當(dāng)然很多場(chǎng)景下,這個(gè)條件不是顯式的,例如前面 addOne 的例子中,set(get()+1) 這個(gè)復(fù)合操作,其實(shí)就隱式依賴(lài) get() 的結(jié)果。
那面對(duì)數(shù)據(jù)競(jìng)爭(zhēng)和競(jìng)態(tài)條件問(wèn)題,又該如何保證線(xiàn)程的安全性呢?其實(shí)這兩類(lèi)問(wèn)題,都可以用互斥這個(gè)技術(shù)方案,而實(shí)現(xiàn)互斥的方案有很多,CPU 提供了相關(guān)的互斥指令,操作系統(tǒng)、編程語(yǔ)言也會(huì)提供相關(guān)的 API。從邏輯上來(lái)看,我們可以統(tǒng)一歸為:鎖。前面幾章我們也粗略地介紹了如何使用鎖,相信你已經(jīng)胸中有丘壑了,這里就不再贅述了,你可以結(jié)合前面的文章溫故知新。
所謂活躍性問(wèn)題,指的是某個(gè)操作無(wú)法執(zhí)行下去。我們常見(jiàn)的“死鎖”就是一種典型的活躍性問(wèn)題,當(dāng)然除了死鎖外,還有兩種情況,分別是“活鎖”和“饑餓”。通過(guò)前面的學(xué)習(xí)你已經(jīng)知道,發(fā)生“死鎖”后線(xiàn)程會(huì)互相等待,而且會(huì)一直等待下去,在技術(shù)上的表現(xiàn)形式是線(xiàn)程永久地“阻塞”了。
但有時(shí)線(xiàn)程雖然沒(méi)有發(fā)生阻塞,但仍然會(huì)存在執(zhí)行不下去的情況,這就是所謂的“活鎖”??梢灶?lèi)比現(xiàn)實(shí)世界里的例子,路人甲從左手邊出門(mén),路人乙從右手邊進(jìn)門(mén),兩人為了不相撞,互相謙讓?zhuān)啡思鬃屄纷哂沂诌?,路人乙也讓路走左手邊,結(jié)果是兩人又相撞了。這種情況,基本上謙讓幾次就解決了,因?yàn)槿藭?huì)交流啊??墒侨绻@種情況發(fā)生在編程世界了,就有可能會(huì)一直沒(méi)完沒(méi)了地“謙讓”下去,成為沒(méi)有發(fā)生阻塞但依然執(zhí)行不下去的“活鎖”。
解決“活鎖”的方案很簡(jiǎn)單,謙讓時(shí),嘗試等待一個(gè)隨機(jī)的時(shí)間就可以了。例如上面的那個(gè)例子,路人甲走左手邊發(fā)現(xiàn)前面有人,并不是立刻換到右手邊,而是等待一個(gè)隨機(jī)的時(shí)間后,再換到右手邊;同樣,路人乙也不是立刻切換路線(xiàn),也是等待一個(gè)隨機(jī)的時(shí)間再切換。由于路人甲和路人乙等待的時(shí)間是隨機(jī)的,所以同時(shí)相撞后再次相撞的概率就很低了?!暗却粋€(gè)隨機(jī)時(shí)間”的方案雖然很簡(jiǎn)單,卻非常有效,Raft 這樣知名的分布式一致性算法中也用到了它。
那“饑餓”該怎么去理解呢?所謂“饑餓”指的是線(xiàn)程因無(wú)法訪(fǎng)問(wèn)所需資源而無(wú)法執(zhí)行下去的情況?!安换脊眩疾痪?,如果線(xiàn)程優(yōu)先級(jí)“不均”,在 CPU 繁忙的情況下,優(yōu)先級(jí)低的線(xiàn)程得到執(zhí)行的機(jī)會(huì)很小,就可能發(fā)生線(xiàn)程“饑餓”;持有鎖的線(xiàn)程,如果執(zhí)行的時(shí)間過(guò)長(zhǎng),也可能導(dǎo)致“饑餓”問(wèn)題。
解決“饑餓”問(wèn)題的方案很簡(jiǎn)單,有三種方案:一是保證資源充足,二是公平地分配資源,三就是避免持有鎖的線(xiàn)程長(zhǎng)時(shí)間執(zhí)行。這三個(gè)方案中,方案一和方案三的適用場(chǎng)景比較有限,因?yàn)楹芏鄨?chǎng)景下,資源的稀缺性是沒(méi)辦法解決的,持有鎖的線(xiàn)程執(zhí)行的時(shí)間也很難縮短。倒是方案二的適用場(chǎng)景相對(duì)來(lái)說(shuō)更多一些。
那如何公平地分配資源呢?在并發(fā)編程里,主要是使用公平鎖。所謂公平鎖,是一種先來(lái)后到的方案,線(xiàn)程的等待是有順序的,排在等待隊(duì)列前面的線(xiàn)程會(huì)優(yōu)先獲得資源。
使用“鎖”要非常小心,但是如果小心過(guò)度,也可能出“性能問(wèn)題”?!版i”的過(guò)度使用可能導(dǎo)致串行化的范圍過(guò)大,這樣就不能夠發(fā)揮多線(xiàn)程的優(yōu)勢(shì)了,而我們之所以使用多線(xiàn)程搞并發(fā)程序,為的就是提升性能。
所以我們要盡量減少串行,那串行對(duì)性能的影響是怎么樣的呢?假設(shè)串行百分比是 5%,我們用多核多線(xiàn)程相比單核單線(xiàn)程能提速多少呢?
有個(gè)阿姆達(dá)爾(Amdahl)定律,代表了處理器并行運(yùn)算之后效率提升的能力,它正好可以解決這個(gè)問(wèn)題,具體公式如下:
公式里的 n 可以理解為 CPU 的核數(shù),p 可以理解為并行百分比,那(1-p)就是串行百分比了,也就是我們假設(shè)的 5%。我們?cè)偌僭O(shè) CPU 的核數(shù)(也就是 n)無(wú)窮大,那加速比 S的極限就是 20。也就是說(shuō),如果我們的串行率是 5%,那么我們無(wú)論采用什么技術(shù),最高也就只能提高 20 倍的性能。
所以使用鎖的時(shí)候一定要關(guān)注對(duì)性能的影響。 那怎么才能避免鎖帶來(lái)的性能問(wèn)題呢?這個(gè)問(wèn)題很復(fù)雜,Java SDK 并發(fā)包里之所以有那么多東西,有很大一部分原因就是要提升在某個(gè)特定領(lǐng)域的性能。
不過(guò)從方案層面,我們可以這樣來(lái)解決這個(gè)問(wèn)題。
第一,既然使用鎖會(huì)帶來(lái)性能問(wèn)題,那最好的方案自然就是使用無(wú)鎖的算法和數(shù)據(jù)結(jié)構(gòu)了。在這方面有很多相關(guān)的技術(shù),例如線(xiàn)程本地存儲(chǔ) (Thread Local Storage, TLS)、寫(xiě)入時(shí)復(fù)制 (Copy-on-write)、樂(lè)觀(guān)鎖等;Java 并發(fā)包里面的原子類(lèi)也是一種無(wú)鎖的數(shù)據(jù)結(jié)構(gòu);Disruptor 則是一個(gè)無(wú)鎖的內(nèi)存隊(duì)列,性能都非常好……
第二,減少鎖持有的時(shí)間。互斥鎖本質(zhì)上是將并行的程序串行化,所以要增加并行度,一定要減少持有鎖的時(shí)間。這個(gè)方案具體的實(shí)現(xiàn)技術(shù)也有很多,例如使用細(xì)粒度的鎖,一個(gè)典型的例子就是 Java 并發(fā)包里的 ConcurrentHashMap,它使用了所謂分段鎖的技術(shù)(這個(gè)技術(shù)后面我們會(huì)詳細(xì)介紹);還可以使用讀寫(xiě)鎖,也就是讀是無(wú)鎖的,只有寫(xiě)的時(shí)候才會(huì)互斥。
性能方面的度量指標(biāo)有很多,我覺(jué)得有三個(gè)指標(biāo)非常重要,就是:吞吐量、延遲和并發(fā)量。
吞吐量:指的是單位時(shí)間內(nèi)能處理的請(qǐng)求數(shù)量。吞吐量越高,說(shuō)明性能越好。
延遲:指的是從發(fā)出請(qǐng)求到收到響應(yīng)的時(shí)間。延遲越小,說(shuō)明性能越好。
并發(fā)量:指的是能同時(shí)處理的請(qǐng)求數(shù)量,一般來(lái)說(shuō)隨著并發(fā)量的增加、延遲也會(huì)增加。所以延遲這個(gè)指標(biāo),一般都會(huì)是基于并發(fā)量來(lái)說(shuō)的。例如并發(fā)量是 1000 的時(shí)候,延遲是 50 毫秒。
并發(fā)編程是一個(gè)復(fù)雜的技術(shù)領(lǐng)域,微觀(guān)上涉及到原子性問(wèn)題、可見(jiàn)性問(wèn)題和有序性問(wèn)題,宏觀(guān)則表現(xiàn)為安全性、活躍性以及性能問(wèn)題。
我們?cè)谠O(shè)計(jì)并發(fā)程序的時(shí)候,主要是從宏觀(guān)出發(fā),也就是要重點(diǎn)關(guān)注它的安全性、活躍性以及性能。安全性方面要注意數(shù)據(jù)競(jìng)爭(zhēng)和競(jìng)態(tài)條件,活躍性方面需要注意死鎖、活鎖、饑餓等問(wèn)題,性能方面我們雖然介紹了兩個(gè)方案,但是遇到具體問(wèn)題,你還是要具體分析,根據(jù)特定的場(chǎng)景選擇合適的數(shù)據(jù)結(jié)構(gòu)和算法。
到此,關(guān)于“并發(fā)編程中我們需要注意的問(wèn)題有哪些”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!