- Java并發(fā)編程深度解析與實(shí)戰(zhàn)
- 譚鋒(Mic)
- 1153字
- 2022-05-10 18:39:16
2.1 揭秘多線程環(huán)境下的原子性問(wèn)題
什么是原子性呢?
在數(shù)據(jù)庫(kù)事務(wù)的ACID特性中就有原子性,它是指當(dāng)前操作中包含的多個(gè)數(shù)據(jù)庫(kù)事務(wù)操作,要么全部成功,要么全部失敗,不允許存在部分成功、部分失敗的情況。而在多線程中的原子性與數(shù)據(jù)庫(kù)事務(wù)的原子性相同,它是指一個(gè)或多個(gè)指令操作在CPU執(zhí)行過(guò)程中不允許被中斷。
下面我們來(lái)演示一個(gè)多線程中出現(xiàn)原子性問(wèn)題的例子。

在上述代碼中啟動(dòng)了兩個(gè)線程,每個(gè)線程對(duì)成員變量i累加10000次,然后打印出累加后的結(jié)果。我們從結(jié)果中發(fā)現(xiàn),原本期望的值是20000,但是打印出來(lái)的i值都是一個(gè)小于20000的數(shù),和預(yù)期的結(jié)果不一致,導(dǎo)致這個(gè)現(xiàn)象產(chǎn)生的原因就是原子性問(wèn)題。
2.1.1 深入分析原子性問(wèn)題的本質(zhì)
從本質(zhì)上說(shuō),原子性問(wèn)題產(chǎn)生的原因有兩個(gè)。
? CPU時(shí)間片切換。
? 執(zhí)行指令的原子性,也就是線程運(yùn)行的程序或者指令是否具備原子性。
CPU時(shí)間片切換
在第1章中,筆者詳述了CPU時(shí)間片切換的原理,也就是當(dāng)CPU不管因?yàn)楹畏N原因處于空閑狀態(tài)時(shí),CPU會(huì)把自己的時(shí)間片分配給其他線程來(lái)處理,整體過(guò)程如圖2-1所示,CPU通過(guò)上下文切換來(lái)提升資源利用率。

圖2-1 CPU時(shí)間片切換
i++指令的原子性
在Java程序中,i++操作看起來(lái)是一個(gè)完整的不可分割的指令,但是實(shí)際上并不是這樣的。我們通過(guò)javap -v命令來(lái)查看AtomicExample類中incr()方法的字節(jié)碼,運(yùn)行結(jié)果如下。

可以發(fā)現(xiàn),i++操作實(shí)際上是三個(gè)指令:getfield、iadd、putfield。
? getfield,把變量i從內(nèi)存加載到CPU的寄存器中。
? iadd,在寄存器中執(zhí)行+1操作。
? putfield,把結(jié)果保存到內(nèi)存。
需要注意,這三個(gè)指令并不具備原子性,也就是說(shuō),CPU在執(zhí)行的過(guò)程中會(huì)存在中斷的情況,這種中斷就會(huì)導(dǎo)致原子性問(wèn)題。
如圖2-2所示,假設(shè)有兩個(gè)線程同時(shí)對(duì)變量i進(jìn)行修改,那么可能的執(zhí)行過(guò)程如下:
? 線程1先獲得CPU的執(zhí)行權(quán),在CPU將i=0加載到寄存器中后出現(xiàn)線程切換,CPU把執(zhí)行權(quán)切換給線程2并保留當(dāng)前的CPU上下文。
? 線程2同樣去內(nèi)存中將i加載到寄存器中進(jìn)行計(jì)算,然后把計(jì)算結(jié)果寫回內(nèi)存。
? 線程2釋放了CPU資源,線程1重新獲得執(zhí)行權(quán)后恢復(fù)CPU上下文,而這時(shí)i的值還是0。
? 最終計(jì)算后i的結(jié)果比預(yù)期結(jié)果要小。

圖2-2 線程切換導(dǎo)致原子性問(wèn)題
除上述這種情況外,在多核CPU中,線程的并行執(zhí)行也會(huì)導(dǎo)致原子性問(wèn)題。如圖2-3所示,兩個(gè)線程并行執(zhí)行,同時(shí)從內(nèi)存中將i加載到寄存器中并進(jìn)行計(jì)算,最終導(dǎo)致i的結(jié)果小于我們的預(yù)期值。

圖2-3 多線程并行執(zhí)行導(dǎo)致原子性問(wèn)題
2.1.2 關(guān)于原子性問(wèn)題的解決辦法
通過(guò)上述問(wèn)題的分析,我們發(fā)現(xiàn),多線程環(huán)境下線程的并行或切換導(dǎo)致最終執(zhí)行結(jié)果不符合預(yù)期,解決問(wèn)題的辦法可以從兩個(gè)方面考慮。
? 不允許當(dāng)前非原子指令在執(zhí)行過(guò)程中被中斷,也就是說(shuō)保證i++操作在執(zhí)行過(guò)程中不存在上下文切換。
? 多線程并行執(zhí)行導(dǎo)致的原子性問(wèn)題可以通過(guò)一個(gè)互斥條件來(lái)實(shí)現(xiàn)串行執(zhí)行。
在Java中,synchronized關(guān)鍵字提供了這樣一個(gè)功能,在incr()方法上增加synchronized關(guān)鍵字后,可以保證下面這段代碼中i變量最終的輸出結(jié)果必然是20000。


- scikit-learn Cookbook
- 架構(gòu)不再難(全5冊(cè))
- 編程珠璣(續(xù))
- Practical Windows Forensics
- Mastering C# Concurrency
- C語(yǔ)言程序設(shè)計(jì)案例式教程
- The HTML and CSS Workshop
- Reactive Android Programming
- 組態(tài)軟件技術(shù)與應(yīng)用
- Active Directory with PowerShell
- 微信小程序全棧開(kāi)發(fā)技術(shù)與實(shí)戰(zhàn)(微課版)
- Procedural Content Generation for C++ Game Development
- 硬件產(chǎn)品設(shè)計(jì)與開(kāi)發(fā):從原型到交付
- Learning Android Application Testing
- Mastering Concurrency in Python