7張圖帶你輕松理解Java 線程安全
? ? ?
? ?正文? ?
/? ?什么是線程? ?/
按操作系統(tǒng)中的描述,線程是 CPU 調(diào)度的最小單元,直觀來說線程就是代碼按順序執(zhí)行下來,執(zhí)行完畢就結(jié)束的一條線。
舉個 ?,富土康的一個組裝車間相當(dāng)于 CPU ,而線程就是當(dāng)前車間里的一條條作業(yè)流水線。為了提高產(chǎn)能和效率,車間里一般都會有多條流水線同時作業(yè)。同樣在我們 Android 開發(fā)中多線程可以說是隨處可見了,如執(zhí)行耗時操作,網(wǎng)絡(luò)請求、文件讀寫、數(shù)據(jù)庫讀寫等等都會開單獨的子線程來執(zhí)行。
那么你的線程是安全的嗎?線程安全的原理又是什么呢?(本文內(nèi)容是個人學(xué)習(xí)總結(jié)淺見,如有錯誤的地方,望大佬們輕拍指正)
/? ?線程安全? ?/
了解線程安全的之前先來了解一下 Java 的內(nèi)存模型,先搞清楚線程是怎么工作的。
?Java 內(nèi)存模型 - JMM
什么是 JMM
JMM(Java Memory Model),是一種基于計算機內(nèi)存模型(定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范),屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范。保證共享內(nèi)存的原子性、可見性、有序性。
能用圖的地方盡量不廢話,先來看一張圖:
上圖描述了一個多線程執(zhí)行場景。線程 A 和線程 B 分別對主內(nèi)存的變量進行讀寫操作。其中主內(nèi)存中的變量為共享變量,也就是說此變量只此一份,多個線程間共享。但是線程不能直接讀寫主內(nèi)存的共享變量,每個線程都有自己的工作內(nèi)存,線程需要讀寫主內(nèi)存的共享變量時需要先將該變量拷貝一份副本到自己的工作內(nèi)存,然后在自己的工作內(nèi)存中對該變量進行所有操作,線程工作內(nèi)存對變量副本完成操作之后需要將結(jié)果同步至主內(nèi)存。
線程的工作內(nèi)存是線程私有內(nèi)存,線程間無法互相訪問對方的工作內(nèi)存。
為了便于理解,用圖來描述一下線程對變量賦值的流程。
那么問題來了,線程工作內(nèi)存怎么知道什么時候又是怎樣將數(shù)據(jù)同步到主內(nèi)存呢?這里就輪到 JMM 出場了。JMM 規(guī)定了何時以及如何做線程工作內(nèi)存與主內(nèi)存之間的數(shù)據(jù)同步。
對 JMM 有了初步的了解,簡單總結(jié)一下原子性、可見性、有序性。
原子性:對共享內(nèi)存的操作必須是要么全部執(zhí)行直到執(zhí)行結(jié)束,且中間過程不能被任何外部因素打斷,要么就不執(zhí)行。
可見性:多線程操作共享內(nèi)存時,執(zhí)行結(jié)果能夠及時的同步到共享內(nèi)存,確保其他線程對此結(jié)果及時可見。
有序性:程序的執(zhí)行順序按照代碼順序執(zhí)行,在單線程環(huán)境下,程序的執(zhí)行都是有序的,但是在多線程環(huán)境下,JMM 為了性能優(yōu)化,編譯器和處理器會對指令進行重排,程序的執(zhí)行會變成無序。
到這里,我們可以引出本文的主題了 --【線程安全】。
線程安全的本質(zhì)
其實第一張圖的例子是有問題的,主內(nèi)存中的變量是共享的,所有線程都可以訪問讀寫,而線程工作內(nèi)存又是線程私有的,線程間不可互相訪問。那在多線程場景下,圖上的線程 A 和線程 B 同時來操做共享內(nèi)存里的同一個變量,那么主內(nèi)存內(nèi)的此變量數(shù)據(jù)就會被破壞。也就是說主內(nèi)存內(nèi)的此變量不是線程安全的。我們來看個代碼小例子幫助理解。
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個線程分別執(zhí)行 1_000_000 次 count() 方法, count() 方法中只執(zhí)行簡單的 x++ 操作,理論上每次執(zhí)行 runTest 方法應(yīng)該有一個線程輸出的 x 結(jié)果應(yīng)該是2_000_000。但實際的運行結(jié)果并非我們所想:
final?x?from?1:?989840
final?x?from?2:?1872479
我運行了10次,其中一個線程輸出 x 的值為 2_000_000 只出現(xiàn)了2次。
final?x?from?1:?1000000
final?x?from?2:?2000000
出現(xiàn)這樣的結(jié)果的原因也就是我們上面所說的,在多線程環(huán)境下,我們主內(nèi)存的 x 變量的數(shù)據(jù)被破壞了。我們都知道完成一次 i++ 相當(dāng)于執(zhí)行了:
int?tmp?=?x?+?1;
x?=?tmp;
在多線程環(huán)境下就會出現(xiàn)在執(zhí)行完 int tmp = x + 1; 這行代碼時就發(fā)生了線程切換,當(dāng)線程再次切回來的時候,x 就會被重復(fù)賦值,導(dǎo)致出現(xiàn)上面的運行結(jié)果,2個線程都無法輸出 2_000_000。
下圖描述了示例代碼的執(zhí)行時序:
那么 Java 是如何來解決上述問題來保證線程安全,保證共享內(nèi)存的原子性、可見性、有序性的呢?
/? ?線程同步? ?/
Java 提供了一系列的關(guān)鍵字和類來保證線程安全。
Synchronized 關(guān)鍵字
Synchronized 作用
保證方法或代碼塊操作的原子性
Synchronized 保證?法內(nèi)部或代碼塊內(nèi)部資源(數(shù)據(jù))的互斥訪問。即同?時間、由同?個 Monitor(監(jiān)視鎖) 監(jiān)視的代碼,最多只能有?個線程在訪問。
話不多說來張動圖描述一下 Monitor 工作機制:
被 Synchronized 關(guān)鍵字描述的方法或代碼塊在多線程環(huán)境下同一時間只能由一個線程進行訪問,在持有當(dāng)前 Monitor 的線程執(zhí)行完成之前,其他線程想要調(diào)用相關(guān)方法就必須進行排隊,知道持有持有當(dāng)前 Monitor 的線程執(zhí)行結(jié)束,釋放 Monitor ,下一個線程才可獲取 Monitor 執(zhí)行。
如果存在多個 Monitor 的情況時,多個 Monitor 之間是不互斥的。
多個 Monitor 的情況出現(xiàn)在自定義多個鎖分別來描述不同的方法或代碼塊,Synchronized 在描述代碼塊時可以指定自定義 Monitor ,默認為 this 即當(dāng)前類。
保證監(jiān)視資源的可見性
保證多線程環(huán)境下對監(jiān)視資源的數(shù)據(jù)同步。即任何線程在獲取到 Monitor 后的第?時 間,會先將共享內(nèi)存中的數(shù)據(jù)復(fù)制到??的緩存中;任何線程在釋放 Monitor 的第? 時間,會先將緩存中的數(shù)據(jù)復(fù)制到共享內(nèi)存中。
保證線程間操作的有序性
Synchronized 的原子性保證了由其描述的方法或代碼操作具有有序性,同一時間只能由最多只能有一個線程訪問,不會觸發(fā) JMM 指令重排機制。
Volatile 關(guān)鍵字
Volatile 作用
保證被 Volatile 關(guān)鍵字描述變量的操作具有可見性和有序性(禁止指令重排)。
注意:
Volatile 只對基本類型 (byte、char、short、int、long、float、double、boolean) 的賦值 操作和對象的引?賦值操作有效。
對于 i++ 此類復(fù)合操作, Volatile 無法保證其有序性和原子性。
相對 Synchronized 來說 Volatile 更加輕量一些。
java.util.concurrent.atomic
包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等類。使用這些類來聲明變量可以保證對其操作具有原子性來保證線程安全。
實現(xiàn)原理上與 Synchronized 使用 Monitor(監(jiān)視鎖)保證資源在多線程環(huán)境下阻塞互斥訪問不同,java.util.concurrent.atomic 包下的各原子類基于 CAS(CompareAndSwap) 操作原理實現(xiàn)。?
CAS 又稱無鎖操作,一種樂觀鎖策略,原理就是多線程環(huán)境下各線程訪問共享變量不會加鎖阻塞排隊,線程不會被掛起。通俗來講就是一直循環(huán)對比,如果有訪問沖突則重試,直到?jīng)]有沖突為止。
Lock
Lock 也是 java.util.concurrent 包下的一個接口,定義了一系列的鎖操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 實現(xiàn)類。與 Synchronized 不同是 Lock 提供了獲取鎖和釋放鎖等相關(guān)接口,使得使用上更加靈活,同時也可以做更加復(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)線程安全問題的原因:
在多個線程并發(fā)環(huán)境下,多個線程共同訪問同一共享內(nèi)存資源時,其中一個線程對資源進行寫操作的中途(寫?入已經(jīng)開始,但還沒 結(jié)束),其他線程對這個寫了一半的資源進?了讀操作,或者對這個寫了一半的資源進?了寫操作,導(dǎo)致此資源出現(xiàn)數(shù)據(jù)錯誤。
如何避免線程安全問題?
保證共享資源在同一時間只能由一個線程進行操作(原子性,有序性)。
將線程操作的結(jié)果及時刷新,保證其他線程可以立即獲取到修改后的最新數(shù)據(jù)(可見性)。
來源:juejin.im/post/5d2c97bff265da1bc552954b
版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認,我們都會標(biāo)明作者及出處,如有侵權(quán)煩請告知,我們會立即刪除并表示歉意。謝謝!








