在并發(fā)編程中,所有問題的根源就是可見性、原子性和有序性問題,這篇文章我們就來聊聊原子性問題。
在介紹原子性問題之前,先來說下線程安全:
線程安全
我理解的線程安全就是不管單線程還是多線程并發(fā)的時(shí)候,始終能保證運(yùn)行的正確性,那么這個(gè)類就是線程安全的。
其中在《Java并發(fā)編程實(shí)戰(zhàn)》一書中對(duì)線程安全的定義如下:
當(dāng)多個(gè)線程訪問某個(gè)類時(shí),不管運(yùn)行是環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個(gè)類都能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的。
為了保證線程安全,可能會(huì)有很多的挑戰(zhàn)和問題,當(dāng)我們了解了問題根源所在,問題也就迎刃而解了,接下來介紹線程安全三大特性之一的原子性。
原子性
原子,我想大家應(yīng)該都有印象吧,在化學(xué)反應(yīng)中不可再分的基本微粒就是原子,也就是不可分割。
同時(shí)事務(wù)的四大特性 ACID 中也有原子性,那么原子性究竟是什么呢?
原子性其實(shí)就是所有操作要么全部成功,要么全部失敗,這些操作是不可拆分的,也可以簡單地理解為不可分割性。
將整個(gè)操作視作一個(gè)整體是原子性的核心特征,這些操作就是原子性操作。
接下來舉個(gè)原子性操作在生活中的例子:
比如,wupx 今天剛發(fā)了 5100 元的工資,全身家當(dāng)為 5100 元,huxy 目前余額還有 1000 元,此時(shí) wupx 上交 5000 元,如果轉(zhuǎn)賬成功,則 huxy 的余額就變?yōu)榱?6000 元,wupx 的余額為 100 元。
若轉(zhuǎn)賬失敗,則轉(zhuǎn)出去的余額會(huì)退回來,wupx 的余額仍然是 5100 元,huxy 的余額為 1000 元。
不會(huì)出現(xiàn) wupx 的錢轉(zhuǎn)出去了,huxy 的余額沒有增加,或者 wupx 的工資沒轉(zhuǎn)出去,而 huxy 的余額卻增加的情況。
wupx 上交工資給 huxy 的操作就是原子性操作,wupx 余額減少 5000 元,而 huxy 的余額增加 5000 元的操作是不可分割和拆分的,正如我們上面說到的:要么全部成功,要么全部失敗。wupx 給 huxy 上交成功流程如下所示:
原子操作可以是一個(gè)步驟,也可以是多個(gè)操作步驟,但是其順序不可以被打亂,也不可以被切割而只執(zhí)行其中的一部分。
到這里,我相信大家對(duì)原子性有了基本的了解,下面來聊下原子性問題。
原子性問題
原子性問題的核心就是線程切換導(dǎo)致的,因?yàn)椴l(fā)編程中,線程數(shù)設(shè)置的數(shù)目一般會(huì)大于 CPU 核心數(shù)。
關(guān)于線程數(shù)的設(shè)置可以閱讀:線程數(shù),射多少更舒適?
每個(gè) CPU 同一時(shí)刻只能被一個(gè)線程使用,而 CPU 資源分配采用的是時(shí)間片輪轉(zhuǎn)策略,也就是給每個(gè)線程分配一個(gè)時(shí)間片,線程在這個(gè)時(shí)間片內(nèi)占用 CPU 的資源來執(zhí)行任務(wù),當(dāng)過了一個(gè)時(shí)間片后,操作系統(tǒng)會(huì)重新選擇一個(gè)線程來執(zhí)行任務(wù),這個(gè)過程一般稱為任務(wù)切換,也叫做線程切換或者線程上下文切換。
上圖就是線程切換的例子,有 Thread-0 和 Thread-1 兩個(gè)線程,其中粉色矩形表示該線程占有 CPU 資源并執(zhí)行任務(wù),剛開始 Thread-1 執(zhí)行一段時(shí)間,這段時(shí)間稱為時(shí)間片,在該時(shí)間片內(nèi),Thread-1 會(huì)占有 CPU 資源并執(zhí)行任務(wù),當(dāng)經(jīng)過一個(gè)時(shí)間片后,Thread-1 會(huì)讓出 CPU 資源,虛線部分表示讓出 CPU,不占用 CPU 資源,CPU 會(huì)重新選擇一個(gè)線程 Thread-0 來執(zhí)行,CPU 會(huì)在 Thread-0 和 Thread-1 之間來回切換,反復(fù)橫跳。
下面通過一個(gè)例子來看下原子性問題,具體代碼如下:
public class AtomicityDemo {
private long count = 0;
public void calc() {
count++;
}
}
calc() 方法中只有一個(gè) count++ 操作,那么就是原子性的嗎?
下面在 class 目錄下使用 javap -c AtomicityDemo 就可以得到如下結(jié)果:
public class com.`wupx`.thread.AtomicityDemo {
public com.`wupx`.thread.AtomicityDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: lconst_0
6: putfield #2 // Field count:J
9: return
public void calc();
Code:
0: aload_0
1: dup
2: getfield #2 // Field count:J
5: lconst_1
6: ladd
7: putfield #2 // Field count:J
10: return
}
重點(diǎn)來看下 calc 方法,這些 CPU 指令大概可以分為如下三步:
- 指令 1:將 count 從內(nèi)存加載到 CPU 寄存器
- 指令 2:在寄存器中執(zhí)行 +1 操作
- 指令 3:將結(jié)果寫入內(nèi)存(也有可能是 CPU 緩存)
關(guān)于 CPU 緩存可以閱讀:原來 CPU 為程序性能優(yōu)化做了這么多
操作系統(tǒng)的線程切換并不是一定是發(fā)生一條語句執(zhí)行完成后,而可能是發(fā)生在任何一條 CPU 執(zhí)行完成后。比如 Thread-0 執(zhí)行完指令 1 后,操作系統(tǒng)發(fā)生了線程切換,兩個(gè)線程都執(zhí)行了 count++ 操作,但是最后的結(jié)果是 1 而不是 2,下面用圖來表示這個(gè)過程。
通過上圖,我們可以發(fā)現(xiàn):Thread-1 將 count=0 加載到 CPU 的寄存器后,發(fā)生了線程切換,此時(shí)內(nèi)存中的 count 值為 0,Thread-0 將 count=0 加載到 CPU 寄存器,執(zhí)行 count++ 操作,并將 count=1 寫到內(nèi)存,此時(shí),CPU 切換到 Thread-1,執(zhí)行 Thread-1 中的 count++ 操作后,Thread-1 中的 count 值為 1,Thread-1 將 count=1 寫入內(nèi)存,此時(shí)內(nèi)存中的 count 值為 1。
因此,在并發(fā)編程中,若在 CPU 中存在正在執(zhí)行的線程,正好 CPU 發(fā)生了線程切換,則可能會(huì)導(dǎo)致原子性問題,這就是導(dǎo)致并發(fā)編程問題的根源之一。
針對(duì)原子性問題,我們可以通過為操作加鎖或者使用原子變量來解決,原子變量在 java.util.concurrent.atomic 包中,是 JDK 1.5 引入的,它提供了一系列的原子操作。
總結(jié)
這篇文章簡要介紹了線程安全的概念,并詳細(xì)介紹了線程安全的特性之一原子性,并針對(duì)原子性問題進(jìn)行了分析。
只有掌握了引發(fā)原子性問題的根源,才能便于我們編寫更加安全的并發(fā)程序。
歡迎大家留言討論,分享你的想法。
參考
《Java并發(fā)編程實(shí)戰(zhàn)》
Java并發(fā)編程實(shí)戰(zhàn)