7張圖帶你輕松理解Java 線(xiàn)程安全
鏈接:juejin.im/post/5d2c97bff265da1bc552954b
??? ?
? ?正文 ??
/? ?什么是線(xiàn)程? ?/
按操作系統(tǒng)中的描述,線(xiàn)程是 CPU 調(diào)度的最小單元,直觀(guān)來(lái)說(shuō)線(xiàn)程就是代碼按順序執(zhí)行下來(lái),執(zhí)行完畢就結(jié)束的一條線(xiàn)。
舉個(gè) ?,富土康的一個(gè)組裝車(chē)間相當(dāng)于 CPU ,而線(xiàn)程就是當(dāng)前車(chē)間里的一條條作業(yè)流水線(xiàn)。為了提高產(chǎn)能和效率,車(chē)間里一般都會(huì)有多條流水線(xiàn)同時(shí)作業(yè)。同樣在我們 Android 開(kāi)發(fā)中多線(xiàn)程可以說(shuō)是隨處可見(jiàn)了,如執(zhí)行耗時(shí)操作,網(wǎng)絡(luò)請(qǐng)求、文件讀寫(xiě)、數(shù)據(jù)庫(kù)讀寫(xiě)等等都會(huì)開(kāi)單獨(dú)的子線(xiàn)程來(lái)執(zhí)行。
那么你的線(xiàn)程是安全的嗎?線(xiàn)程安全的原理又是什么呢?(本文內(nèi)容是個(gè)人學(xué)習(xí)總結(jié)淺見(jiàn),如有錯(cuò)誤的地方,望大佬們輕拍指正)
/? ?線(xiàn)程安全? ?/
了解線(xiàn)程安全的之前先來(lái)了解一下 Java 的內(nèi)存模型,先搞清楚線(xiàn)程是怎么工作的。
?Java 內(nèi)存模型 - JMM
什么是 JMM
JMM(Java Memory Model),是一種基于計(jì)算機(jī)內(nèi)存模型(定義了共享內(nèi)存系統(tǒng)中多線(xiàn)程程序讀寫(xiě)操作行為的規(guī)范),屏蔽了各種硬件和操作系統(tǒng)的訪(fǎng)問(wèn)差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪(fǎng)問(wèn)都能保證效果一致的機(jī)制及規(guī)范。保證共享內(nèi)存的原子性、可見(jiàn)性、有序性。
能用圖的地方盡量不廢話(huà),先來(lái)看一張圖:

上圖描述了一個(gè)多線(xiàn)程執(zhí)行場(chǎng)景。線(xiàn)程 A 和線(xiàn)程 B 分別對(duì)主內(nèi)存的變量進(jìn)行讀寫(xiě)操作。其中主內(nèi)存中的變量為共享變量,也就是說(shuō)此變量只此一份,多個(gè)線(xiàn)程間共享。但是線(xiàn)程不能直接讀寫(xiě)主內(nèi)存的共享變量,每個(gè)線(xiàn)程都有自己的工作內(nèi)存,線(xiàn)程需要讀寫(xiě)主內(nèi)存的共享變量時(shí)需要先將該變量拷貝一份副本到自己的工作內(nèi)存,然后在自己的工作內(nèi)存中對(duì)該變量進(jìn)行所有操作,線(xiàn)程工作內(nèi)存對(duì)變量副本完成操作之后需要將結(jié)果同步至主內(nèi)存。
線(xiàn)程的工作內(nèi)存是線(xiàn)程私有內(nèi)存,線(xiàn)程間無(wú)法互相訪(fǎng)問(wèn)對(duì)方的工作內(nèi)存。
為了便于理解,用圖來(lái)描述一下線(xiàn)程對(duì)變量賦值的流程。

那么問(wèn)題來(lái)了,線(xiàn)程工作內(nèi)存怎么知道什么時(shí)候又是怎樣將數(shù)據(jù)同步到主內(nèi)存呢?這里就輪到 JMM 出場(chǎng)了。JMM 規(guī)定了何時(shí)以及如何做線(xiàn)程工作內(nèi)存與主內(nèi)存之間的數(shù)據(jù)同步。
對(duì) JMM 有了初步的了解,簡(jiǎn)單總結(jié)一下原子性、可見(jiàn)性、有序性。
原子性:對(duì)共享內(nèi)存的操作必須是要么全部執(zhí)行直到執(zhí)行結(jié)束,且中間過(guò)程不能被任何外部因素打斷,要么就不執(zhí)行。
可見(jiàn)性:多線(xiàn)程操作共享內(nèi)存時(shí),執(zhí)行結(jié)果能夠及時(shí)的同步到共享內(nèi)存,確保其他線(xiàn)程對(duì)此結(jié)果及時(shí)可見(jiàn)。
有序性:程序的執(zhí)行順序按照代碼順序執(zhí)行,在單線(xiàn)程環(huán)境下,程序的執(zhí)行都是有序的,但是在多線(xiàn)程環(huán)境下,JMM 為了性能優(yōu)化,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排,程序的執(zhí)行會(huì)變成無(wú)序。
到這里,我們可以引出本文的主題了 --【線(xiàn)程安全】。
線(xiàn)程安全的本質(zhì)
其實(shí)第一張圖的例子是有問(wèn)題的,主內(nèi)存中的變量是共享的,所有線(xiàn)程都可以訪(fǎng)問(wèn)讀寫(xiě),而線(xiàn)程工作內(nèi)存又是線(xiàn)程私有的,線(xiàn)程間不可互相訪(fǎng)問(wèn)。那在多線(xiàn)程場(chǎng)景下,圖上的線(xiàn)程 A 和線(xiàn)程 B 同時(shí)來(lái)操做共享內(nèi)存里的同一個(gè)變量,那么主內(nèi)存內(nèi)的此變量數(shù)據(jù)就會(huì)被破壞。也就是說(shuō)主內(nèi)存內(nèi)的此變量不是線(xiàn)程安全的。我們來(lái)看個(gè)代碼小例子幫助理解。
public?class?ThreadDemo?{
????private?int?x?=?0;
????private?void?count()?{
????????x++;
????}
????public?void?runTest()?{
????????new?Thread()?{
????????????@Override
????????????public?void?run()?{
????????????????for?(int?i?=?0;?i?1_000_000;?i++)?{
????????????????????count();
????????????????}
????????????????System.out.println("final?x?from?1:?"?+?x);
????????????}
????????}.start();
????????new?Thread()?{
????????????@Override
????????????public?void?run()?{
????????????????for?(int?i?=?0;?i?1_000_000;?i++)?{
????????????????????count();
????????????????}
????????????????System.out.println("final?x?from?2:?"?+?x);
????????????}
????????}.start();
????}
????public?static?void?main(String[]?args)?{
????????new?ThreadDemo().runTest();
????}
}
示例代碼中 runTest 方法2個(gè)線(xiàn)程分別執(zhí)行 1_000_000 次 count() 方法, count() 方法中只執(zhí)行簡(jiǎn)單的 x++ 操作,理論上每次執(zhí)行 runTest 方法應(yīng)該有一個(gè)線(xiàn)程輸出的 x 結(jié)果應(yīng)該是2_000_000。但實(shí)際的運(yùn)行結(jié)果并非我們所想:
final?x?from?1:?989840
final?x?from?2:?1872479
我運(yùn)行了10次,其中一個(gè)線(xiàn)程輸出 x 的值為 2_000_000 只出現(xiàn)了2次。
final?x?from?1:?1000000
final?x?from?2:?2000000
出現(xiàn)這樣的結(jié)果的原因也就是我們上面所說(shuō)的,在多線(xiàn)程環(huán)境下,我們主內(nèi)存的 x 變量的數(shù)據(jù)被破壞了。我們都知道完成一次 i++ 相當(dāng)于執(zhí)行了:
int?tmp?=?x?+?1;
x?=?tmp;
在多線(xiàn)程環(huán)境下就會(huì)出現(xiàn)在執(zhí)行完 int tmp = x + 1; 這行代碼時(shí)就發(fā)生了線(xiàn)程切換,當(dāng)線(xiàn)程再次切回來(lái)的時(shí)候,x 就會(huì)被重復(fù)賦值,導(dǎo)致出現(xiàn)上面的運(yùn)行結(jié)果,2個(gè)線(xiàn)程都無(wú)法輸出 2_000_000。
下圖描述了示例代碼的執(zhí)行時(shí)序:

那么 Java 是如何來(lái)解決上述問(wèn)題來(lái)保證線(xiàn)程安全,保證共享內(nèi)存的原子性、可見(jiàn)性、有序性的呢?
/? ?線(xiàn)程同步? ?/
Java 提供了一系列的關(guān)鍵字和類(lèi)來(lái)保證線(xiàn)程安全。
Synchronized 關(guān)鍵字
Synchronized 作用
保證方法或代碼塊操作的原子性
Synchronized 保證?法內(nèi)部或代碼塊內(nèi)部資源(數(shù)據(jù))的互斥訪(fǎng)問(wèn)。即同?時(shí)間、由同?個(gè) Monitor(監(jiān)視鎖) 監(jiān)視的代碼,最多只能有?個(gè)線(xiàn)程在訪(fǎng)問(wèn)。
話(huà)不多說(shuō)來(lái)張動(dòng)圖描述一下 Monitor 工作機(jī)制:


被 Synchronized 關(guān)鍵字描述的方法或代碼塊在多線(xiàn)程環(huán)境下同一時(shí)間只能由一個(gè)線(xiàn)程進(jìn)行訪(fǎng)問(wèn),在持有當(dāng)前 Monitor 的線(xiàn)程執(zhí)行完成之前,其他線(xiàn)程想要調(diào)用相關(guān)方法就必須進(jìn)行排隊(duì),知道持有持有當(dāng)前 Monitor 的線(xiàn)程執(zhí)行結(jié)束,釋放 Monitor ,下一個(gè)線(xiàn)程才可獲取 Monitor 執(zhí)行。
如果存在多個(gè) Monitor 的情況時(shí),多個(gè) Monitor 之間是不互斥的。
多個(gè) Monitor 的情況出現(xiàn)在自定義多個(gè)鎖分別來(lái)描述不同的方法或代碼塊,Synchronized 在描述代碼塊時(shí)可以指定自定義 Monitor ,默認(rèn)為 this 即當(dāng)前類(lèi)。


保證監(jiān)視資源的可見(jiàn)性
保證多線(xiàn)程環(huán)境下對(duì)監(jiān)視資源的數(shù)據(jù)同步。即任何線(xiàn)程在獲取到 Monitor 后的第?時(shí) 間,會(huì)先將共享內(nèi)存中的數(shù)據(jù)復(fù)制到??的緩存中;任何線(xiàn)程在釋放 Monitor 的第? 時(shí)間,會(huì)先將緩存中的數(shù)據(jù)復(fù)制到共享內(nèi)存中。
保證線(xiàn)程間操作的有序性
Synchronized 的原子性保證了由其描述的方法或代碼操作具有有序性,同一時(shí)間只能由最多只能有一個(gè)線(xiàn)程訪(fǎng)問(wèn),不會(huì)觸發(fā) JMM 指令重排機(jī)制。
Volatile 關(guān)鍵字
Volatile 作用
保證被 Volatile 關(guān)鍵字描述變量的操作具有可見(jiàn)性和有序性(禁止指令重排)。
注意:
Volatile 只對(duì)基本類(lèi)型 (byte、char、short、int、long、float、double、boolean) 的賦值 操作和對(duì)象的引?賦值操作有效。
對(duì)于 i++ 此類(lèi)復(fù)合操作, Volatile 無(wú)法保證其有序性和原子性。
相對(duì) Synchronized 來(lái)說(shuō) Volatile 更加輕量一些。
java.util.concurrent.atomic
包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等類(lèi)。使用這些類(lèi)來(lái)聲明變量可以保證對(duì)其操作具有原子性來(lái)保證線(xiàn)程安全。
實(shí)現(xiàn)原理上與 Synchronized 使用 Monitor(監(jiān)視鎖)保證資源在多線(xiàn)程環(huán)境下阻塞互斥訪(fǎng)問(wèn)不同,java.util.concurrent.atomic 包下的各原子類(lèi)基于 CAS(CompareAndSwap) 操作原理實(shí)現(xiàn)。?
CAS 又稱(chēng)無(wú)鎖操作,一種樂(lè)觀(guān)鎖策略,原理就是多線(xiàn)程環(huán)境下各線(xiàn)程訪(fǎng)問(wèn)共享變量不會(huì)加鎖阻塞排隊(duì),線(xiàn)程不會(huì)被掛起。通俗來(lái)講就是一直循環(huán)對(duì)比,如果有訪(fǎng)問(wèn)沖突則重試,直到?jīng)]有沖突為止。
Lock
Lock 也是 java.util.concurrent 包下的一個(gè)接口,定義了一系列的鎖操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 實(shí)現(xiàn)類(lèi)。與 Synchronized 不同是 Lock 提供了獲取鎖和釋放鎖等相關(guān)接口,使得使用上更加靈活,同時(shí)也可以做更加復(fù)雜的操作,如:
ReentrantReadWriteLock?lock?=?new?ReentrantReadWriteLock();
Lock?readLock?=?lock.readLock();
Lock?writeLock?=?lock.writeLock();
private?int?x?=?0;
private?void?count()?{
????writeLock.lock();
????try?{
????????x++;
????}?finally?{
????????writeLock.unlock();
????}
}
private?void?print(int?time)?{
????readLock.lock();
????try?{
????????for?(int?i?=?0;?i?????????????System.out.print(x?+?"?");
????????}
????????System.out.println();
????}?finally?{
????????readLock.unlock();
????}
}
/? ?總結(jié)? ?/
出現(xiàn)線(xiàn)程安全問(wèn)題的原因:
在多個(gè)線(xiàn)程并發(fā)環(huán)境下,多個(gè)線(xiàn)程共同訪(fǎng)問(wèn)同一共享內(nèi)存資源時(shí),其中一個(gè)線(xiàn)程對(duì)資源進(jìn)行寫(xiě)操作的中途(寫(xiě)?入已經(jīng)開(kāi)始,但還沒(méi) 結(jié)束),其他線(xiàn)程對(duì)這個(gè)寫(xiě)了一半的資源進(jìn)?了讀操作,或者對(duì)這個(gè)寫(xiě)了一半的資源進(jìn)?了寫(xiě)操作,導(dǎo)致此資源出現(xiàn)數(shù)據(jù)錯(cuò)誤。
如何避免線(xiàn)程安全問(wèn)題?
保證共享資源在同一時(shí)間只能由一個(gè)線(xiàn)程進(jìn)行操作(原子性,有序性)。
將線(xiàn)程操作的結(jié)果及時(shí)刷新,保證其他線(xiàn)程可以立即獲取到修改后的最新數(shù)據(jù)(可見(jiàn)性)。
