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