新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「愛(ài)笑的架構(gòu)師」,作者雷小帥。轉(zhuǎn)載本文請(qǐng)聯(lián)系愛(ài)笑的架構(gòu)師公眾號(hào)。

創(chuàng)新互聯(lián)公司主營(yíng)堯都網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,app軟件定制開(kāi)發(fā),堯都h5微信小程序開(kāi)發(fā)搭建,堯都網(wǎng)站營(yíng)銷推廣歡迎堯都等地區(qū)企業(yè)咨詢
頭發(fā)很多的程序員:『師父,這個(gè)批量處理接口太慢了,有什么辦法可以優(yōu)化?』
架構(gòu)師:『試試使用多線程優(yōu)化』
第二天
頭發(fā)很多的程序員:『師父,我已經(jīng)使用了多線程,為什么接口還變慢了?』
架構(gòu)師:『去給我買杯咖啡,我寫(xiě)篇文章告訴你』
……吭哧吭哧買咖啡去了
在實(shí)際工作中,錯(cuò)誤使用多線程非但不能提高效率還可能使程序崩潰。以在路上開(kāi)車為例:
在一個(gè)單向行駛的道路上,每輛汽車都遵守交通規(guī)則,這時(shí)候整體通行是正常的?!?jiǎn)蜗蜍嚨馈灰馕吨阂粋€(gè)線程』,『多輛車』意味著『多個(gè)job任務(wù)』。
單線程順利同行
如果需要提升車輛的同行效率,一般的做法就是擴(kuò)展車道,對(duì)應(yīng)程序來(lái)說(shuō)就是『加線程池』,增加線程數(shù)。這樣在同一時(shí)間內(nèi),通行的車輛數(shù)遠(yuǎn)遠(yuǎn)大于單車道。
多線程順利同行
然而成年人的世界沒(méi)有那么完美,車道一旦多起來(lái)『加塞』的場(chǎng)景就會(huì)越來(lái)越多,出現(xiàn)碰撞后也會(huì)影響整條馬路的通行效率。這么一對(duì)比下來(lái)『多車道』確實(shí)可能比『?jiǎn)诬嚨馈灰?/p>
多線程故障
防止汽車頻繁變道加塞可以采取在車道間增加『護(hù)欄』,那在程序的世界該怎么做呢?
程序世界中多線程遇到的問(wèn)題歸納起來(lái)就是三類:『線程安全問(wèn)題』、『活躍性問(wèn)題』、『性能問(wèn)題』,接下來(lái)會(huì)講解這些問(wèn)題,以及問(wèn)題對(duì)應(yīng)的解決手段。
線程安全問(wèn)題
有時(shí)候我們會(huì)發(fā)現(xiàn),明明在單線程環(huán)境中正常運(yùn)行的代碼,在多線程環(huán)境中可能會(huì)出現(xiàn)意料之外的結(jié)果,其實(shí)這就是大家常說(shuō)的『線程不安全』。那到底什么是線程不安全呢?往下看。
原子性
舉一個(gè)銀行轉(zhuǎn)賬的例子,比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元,兩個(gè)操作都成功才意味著一次轉(zhuǎn)賬最終成功。
試想一下,如果這兩個(gè)操作不具備原子性,從A的賬戶扣減了1000元之后,操作突然終止了,賬戶B沒(méi)有增加1000元,那問(wèn)題就大了。
銀行轉(zhuǎn)賬這個(gè)例子有兩個(gè)步驟,出現(xiàn)了意外后導(dǎo)致轉(zhuǎn)賬失敗,說(shuō)明沒(méi)有原子性。
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就都不執(zhí)行。
原子操作:即不會(huì)被線程調(diào)度機(jī)制打斷的操作,沒(méi)有上下文切換。
在并發(fā)編程中很多操作都不是原子操作,出個(gè)小題目:
- i = 0; // 操作1
- i++; // 操作2
- i = j; // 操作3
- i = i + 1; // 操作4
上面這四個(gè)操作中有哪些是原子操作,哪些不是的?不熟悉的人可能認(rèn)為這些都是原子操作,其實(shí)只有操作1是原子操作。
- 操作1:對(duì)基本數(shù)據(jù)類型變量的賦值是原子操作;
- 操作2:包含三個(gè)操作,讀取i的值,將i加1,將值賦給i;
- 操作3:讀取j的值,將j的值賦給i;
- 操作4:包含三個(gè)操作,讀取i的值,將i加1,將值賦給i;
在單線程環(huán)境下上述四個(gè)操作都不會(huì)出現(xiàn)問(wèn)題,但是在多線程環(huán)境下,如果不通過(guò)加鎖操作,往往可能得到意料之外的值。
在Java語(yǔ)言中通過(guò)可以使用synchronize或者lock來(lái)保證原子性。
可見(jiàn)性
talk is cheap,先show一段代碼:
- class Test {
- int i = 50;
- int j = 0;
- public void update() {
- // 線程1執(zhí)行
- i = 100;
- }
- public int get() {
- // 線程2執(zhí)行
- j = i;
- return j;
- }
- }
線程1執(zhí)行update方法將 i 賦值為100,一般情況下線程1會(huì)在自己的工作內(nèi)存中完成賦值操作,卻沒(méi)有及時(shí)將新值刷新到主內(nèi)存中。
這個(gè)時(shí)候線程2執(zhí)行g(shù)et方法,首先會(huì)從主內(nèi)存中讀取i的值,然后加載到自己的工作內(nèi)存中,這個(gè)時(shí)候讀取到i的值是50,再將50賦值給j,最后返回j的值就是50了。原本期望返回100,結(jié)果返回50,這就是可見(jiàn)性問(wèn)題,線程1對(duì)變量i進(jìn)行了修改,線程2沒(méi)有立即看到i的新值。
可見(jiàn)性:指當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
如上圖每個(gè)線程都有屬于自己的工作內(nèi)存,工作內(nèi)存和主內(nèi)存間需要通過(guò)store和load等進(jìn)行交互。
為了解決多線程可見(jiàn)性問(wèn)題,Java語(yǔ)言提供了volatile這個(gè)關(guān)鍵字。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。而普通共享變量不能保證可見(jiàn)性,因?yàn)樽兞勘恍薷暮笫裁磿r(shí)候刷回到主存是不確定的,另外一個(gè)線程讀的可能就是舊值。
當(dāng)然Java的鎖機(jī)制如synchronize和lock也是可以保證可見(jiàn)性的,加鎖可以保證在同一時(shí)刻只有一個(gè)線程在執(zhí)行同步代碼塊,釋放鎖之前會(huì)將變量刷回至主存,這樣也就保證了可見(jiàn)性。
關(guān)于線程不安全的表現(xiàn)還有『有序性』,這個(gè)問(wèn)題會(huì)在后面的文章中深入講解。
活躍性問(wèn)題
上面講到為了解決可見(jiàn)性問(wèn)題,我們可以采取加鎖方式解決,但是如果加鎖使用不當(dāng)也容易引入其他問(wèn)題,比如『死鎖』。
在說(shuō)『死鎖』前我們先引入另外一個(gè)概念:活躍性問(wèn)題。
活躍性是指某件正確的事情最終會(huì)發(fā)生,當(dāng)某個(gè)操作無(wú)法繼續(xù)下去的時(shí)候,就會(huì)發(fā)生活躍性問(wèn)題。
概念是不是有點(diǎn)拗口,如果看不懂也沒(méi)關(guān)系,你可以記住活躍性問(wèn)題一般有這樣幾類:死鎖,活鎖,饑餓問(wèn)題。
(1)死鎖
死鎖是指多個(gè)線程因?yàn)榄h(huán)形的等待鎖的關(guān)系而永遠(yuǎn)的阻塞下去。一圖勝千語(yǔ),不多解釋。
(2)活鎖
死鎖是兩個(gè)線程都在等待對(duì)方釋放鎖導(dǎo)致阻塞。而活鎖的意思是線程沒(méi)有阻塞,還活著呢。
當(dāng)多個(gè)線程都在運(yùn)行并且修改各自的狀態(tài),而其他線程彼此依賴這個(gè)狀態(tài),導(dǎo)致任何一個(gè)線程都無(wú)法繼續(xù)執(zhí)行,只能重復(fù)著自身的動(dòng)作和修改自身的狀態(tài),這種場(chǎng)景就是發(fā)生了活鎖。

如果大家還有疑惑,那我再舉一個(gè)生活中的例子,大家平時(shí)在走路的時(shí)候,迎面走來(lái)一個(gè)人,兩個(gè)人互相讓路,但是又同時(shí)走到了一個(gè)方向,如果一直這樣重復(fù)著避讓,這倆人就是發(fā)生了活鎖,學(xué)到了吧,嘿嘿。
(3)饑餓
如果一個(gè)線程無(wú)其他異常卻遲遲不能繼續(xù)運(yùn)行,那基本是處于饑餓狀態(tài)了。
常見(jiàn)有幾種場(chǎng)景:
- 高優(yōu)先級(jí)的線程一直在運(yùn)行消耗CPU,所有的低優(yōu)先級(jí)線程一直處于等待;
- 一些線程被永久堵塞在一個(gè)等待進(jìn)入同步塊的狀態(tài),而其他線程總是能在它之前持續(xù)地對(duì)該同步塊進(jìn)行訪問(wèn);
有一個(gè)非常經(jīng)典的饑餓問(wèn)題就是哲學(xué)家用餐問(wèn)題,如下圖所示,有五個(gè)哲學(xué)家在用餐,每個(gè)人必須要同時(shí)拿兩把叉子才可以開(kāi)始就餐,如果哲學(xué)家1和哲學(xué)家3同時(shí)開(kāi)始就餐,那哲學(xué)家2、4、5就得餓肚子等待了。
性能問(wèn)題
前面講到了線程安全和死鎖、活鎖這些問(wèn)題會(huì)影響多線程執(zhí)行過(guò)程,如果這些都沒(méi)有發(fā)生,多線程并發(fā)一定比單線程串行執(zhí)行快嗎,答案是不一定,因?yàn)槎嗑€程有創(chuàng)建線程和線程上下文切換的開(kāi)銷。
創(chuàng)建線程是直接向系統(tǒng)申請(qǐng)資源的,對(duì)操作系統(tǒng)來(lái)說(shuō)創(chuàng)建一個(gè)線程的代價(jià)是十分昂貴的,需要給它分配內(nèi)存、列入調(diào)度等。
線程創(chuàng)建完之后,還會(huì)遇到線程上下文切換。
CPU是很寶貴的資源速度也非???,為了保證雨露均沾,通常為給不同的線程分配時(shí)間片,當(dāng)CPU從執(zhí)行一個(gè)線程切換到執(zhí)行另一個(gè)線程時(shí),CPU 需要保存當(dāng)前線程的本地?cái)?shù)據(jù),程序指針等狀態(tài),并加載下一個(gè)要執(zhí)行的線程的本地?cái)?shù)據(jù),程序指針等,這個(gè)開(kāi)關(guān)被稱為『上下文切換』。
一般減少上下文切換的方法有:無(wú)鎖并發(fā)編程、CAS 算法、使用協(xié)程等。
有態(tài)度的總結(jié)
多線程用好了可以讓程序的效率成倍提升,用不好可能比單線程還要慢。
用一張圖總結(jié)一下上面講的:
分享標(biāo)題:10張圖告訴你多線程那些破事
標(biāo)題路徑:http://m.5511xx.com/article/coeepss.html


咨詢
建站咨詢
