前言
在單線程中不會出現(xiàn)線程安全問題,而在多線程編程中,有可能會出現(xiàn)同時訪問同一個?共享、可變資源?的情況,這種資源可以是:一個變量、一個對象、一個文件等。特別注意兩點(diǎn):

簡單的說,如果你的代碼在單線程下執(zhí)行和在多線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果,那么你的代碼就是線程安全的。那么,當(dāng)進(jìn)行多線程編程時,我們又會面臨哪些線程安全的要求呢?又是要如何去解決的呢?
1 線程安全特性
1.1 原子性
跟數(shù)據(jù)庫事務(wù)的原子性概念差不多,即一個操作(有可能包含有多個子操作)要么全部執(zhí)行(生效),要么全部都不執(zhí)行(都不生效)。
關(guān)于原子性,一個非常經(jīng)典的例子就是銀行轉(zhuǎn)賬問題:

1.2 可見性
可見性是指,當(dāng)多個線程并發(fā)訪問共享變量時,一個線程對共享變量的修改,其它線程能夠立即看到。可見性問題是好多人忽略或者理解錯誤的一點(diǎn)。
CPU從主內(nèi)存中讀數(shù)據(jù)的效率相對來說不高,現(xiàn)在主流的計(jì)算機(jī)中,都有幾級緩存。每個線程讀取共享變量時,都會將該變量加載進(jìn)其對應(yīng)CPU的高速緩存里,修改該變量后,CPU會立即更新該緩存,但并不一定會立即將其寫回主內(nèi)存(實(shí)際上寫回主內(nèi)存的時間不可預(yù)期)。此時其它線程(尤其是不在同一個CPU上執(zhí)行的線程)訪問該變量時,從主內(nèi)存中讀到的就是舊的數(shù)據(jù),而非第一個線程更新后的數(shù)據(jù)。
這一點(diǎn)是操作系統(tǒng)或者說是硬件層面的機(jī)制,所以很多應(yīng)用開發(fā)人員經(jīng)常會忽略。
1.3 有序性
有序性指的是,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。以下面這段代碼為例:

從代碼順序上看,上面四條語句應(yīng)該依次執(zhí)行,但實(shí)際上JVM真正在執(zhí)行這段代碼時,并不保證它們一定完全按照此順序執(zhí)行。
處理器為了提高程序整體的執(zhí)行效率,可能會對代碼進(jìn)行優(yōu)化,其中的一項(xiàng)優(yōu)化方式就是調(diào)整代碼順序,按照更高效的順序執(zhí)行代碼。
講到這里,有人要著急了——什么,CPU不按照我的代碼順序執(zhí)行代碼,那怎么保證得到我們想要的效果呢?實(shí)際上,大家大可放心,CPU雖然并不保證完全按照代碼順序執(zhí)行,但它會保證程序最終的執(zhí)行結(jié)果和代碼順序執(zhí)行時的結(jié)果一致。
2 線程安全問題
2.1 競態(tài)條件與臨界區(qū)
線程之間共享堆空間,在編程的時候就要格外注意避免競態(tài)條件。危險在于多個線程同時訪問相同的資源并進(jìn)行讀寫操作。當(dāng)其中一個線程需要根據(jù)某個變量的狀態(tài)來相應(yīng)執(zhí)行某個操作的之前,該變量很可能已經(jīng)被其它線程修改。


2.2 死鎖
死鎖:指兩個或兩個以上的進(jìn)程(或線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
關(guān)于死鎖發(fā)生的條件:

2.3 活鎖
活鎖:是指線程1可以使用資源,但它很禮貌,讓其他線程先使用資源,線程2也可以使用資源,但它很紳士,也讓其他線程先使用資源。這樣你讓我,我讓你,最后兩個線程都無法使用資源。
關(guān)于“死鎖與活鎖”的比喻:

2.4 饑餓
饑餓:是指如果線程T1占用了資源R,線程T2又請求封鎖R,于是T2等待。T3也請求資源R,當(dāng)T1釋放了R上的封鎖后,系統(tǒng)首先批準(zhǔn)了T3的請求,T2仍然等待。然后T4又請求封鎖R,當(dāng)T3釋放了R上的封鎖之后,系統(tǒng)又批準(zhǔn)了T4的請求......,T2可能永遠(yuǎn)等待。
也就是,如果一個線程因?yàn)镃PU時間全部被其他線程搶走而得不到CPU運(yùn)行時間,這種狀態(tài)被稱之為“饑餓”。而該線程被“饑餓致死”正是因?yàn)樗貌坏紺PU運(yùn)行時間的機(jī)會。
關(guān)于“饑餓”的比喻:

在Java中,下面三個常見的原因會導(dǎo)致線程饑餓,如下:
1.高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的CPU時間

2.線程被永久堵塞在一個等待進(jìn)入同步塊的狀態(tài),因?yàn)槠渌€程總是能在它之前持續(xù)地對該同步塊進(jìn)行訪問

3.線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象,因?yàn)槠渌€程總是被持續(xù)地獲得喚醒

2.5 公平
解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運(yùn)行機(jī)會。在Java中實(shí)現(xiàn)公平性方案,需要:

在Java中實(shí)現(xiàn)公平性,雖Java不可能實(shí)現(xiàn)100%的公平性,依然可以通過同步結(jié)構(gòu)在線程間實(shí)現(xiàn)公平性的提高。
首先來學(xué)習(xí)一段簡單的同步態(tài)代碼:

如果有多個線程調(diào)用doSynchronized()方法,在第一個獲得訪問的線程未完成前,其他線程將一直處于阻塞狀態(tài),而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的。
改為使用鎖方式替代同步塊,為了提高等待線程的公平性,我們使用鎖方式來替代同步塊:

注意到doSynchronized()不再聲明為synchronized,而是用lock.lock()和lock.unlock()來替代。下面是用Lock類做的一個實(shí)現(xiàn):

注意到上面對Lock的實(shí)現(xiàn),如果存在多線程并發(fā)訪問lock(),這些線程將阻塞在對lock()方法的訪問上。另外,如果鎖已經(jīng)鎖上(校對注:這里指的是isLocked等于true時),這些線程將阻塞在while(isLocked)循環(huán)的wait()調(diào)用里面。要記住的是,當(dāng)線程正在等待進(jìn)入lock() 時,可以調(diào)用wait()釋放其鎖實(shí)例對應(yīng)的同步鎖,使得其他多個線程可以進(jìn)入lock()方法,并調(diào)用wait()方法。
這回看下doSynchronized(),你會注意到在lock()和unlock()之間的注釋:在這兩個調(diào)用之間的代碼將運(yùn)行很長一段時間。進(jìn)一步設(shè)想,這段代碼將長時間運(yùn)行,和進(jìn)入lock()并調(diào)用wait()來比較的話。這意味著大部分時間用在等待進(jìn)入鎖和進(jìn)入臨界區(qū)的過程是用在wait()的等待中,而不是被阻塞在試圖進(jìn)入lock()方法中。
在早些時候提到過,同步塊不會對等待進(jìn)入的多個線程誰能獲得訪問做任何保障,同樣當(dāng)調(diào)用notify()時,wait()也不會做保障一定能喚醒線程。因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言,沒有任何區(qū)別。
但我們能夠改變這種情況,如下:

下面將上面Lock類轉(zhuǎn)變?yōu)楣芥iFairLock。你會注意到新的實(shí)現(xiàn)和之前的Lock類中的同步和wait()/notify()稍有不同。重點(diǎn)是,每一個調(diào)用lock()的線程都會進(jìn)入一個隊(duì)列,當(dāng)解鎖時,只有隊(duì)列里的第一個線程被允許鎖住FairLock實(shí)例,所有其它的線程都將處于等待狀態(tài),直到他們處于隊(duì)列頭部。如下:

首先注意到lock()方法不在聲明為synchronized,取而代之的是對必需同步的代碼,在synchronized中進(jìn)行嵌套。

還需注意到,QueueObject實(shí)際是一個semaphore。doWait()和doNotify()方法在QueueObject中保存著信號。這樣做以避免一個線程在調(diào)用queueObject.doWait()之前被另一個線程調(diào)用unlock()并隨之調(diào)用queueObject.doNotify()的線程重入,從而導(dǎo)致信號丟失。queueObject.doWait()調(diào)用放置在synchronized(this)塊之外,以避免被monitor嵌套鎖死,所以另外的線程可以解鎖,只要當(dāng)沒有線程在lock方法的synchronized(this)塊中執(zhí)行即可。
最后,注意到queueObject.doWait()在try – catch塊中是怎樣調(diào)用的。在InterruptedException拋出的情況下,線程得以離開lock(),并需讓它從隊(duì)列中移除。
3 如何確保線程安全特性
3.1 如何確保原子性
3.1.1 鎖和同步
常用的保證Java操作原子性的工具是鎖和同步方法(或者同步代碼塊)。使用鎖,可以保證同一時間只有一個線程能拿到鎖,也就保證了同一時間只有一個線程能執(zhí)行申請鎖和釋放鎖之間的代碼。

與鎖類似的是同步方法或者同步代碼塊。使用非靜態(tài)同步方法時,鎖住的是當(dāng)前實(shí)例;使用靜態(tài)同步方法時,鎖住的是該類的Class對象;使用靜態(tài)代碼塊時,鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象。下面是同步代碼塊示例:

無論使用鎖還是synchronized,本質(zhì)都是一樣,通過鎖或同步來實(shí)現(xiàn)資源的排它性,從而實(shí)際目標(biāo)代碼段同一時間只會被一個線程執(zhí)行,進(jìn)而保證了目標(biāo)代碼段的原子性。這是一種以犧牲性能為代價的方法。
3.1.2 CAS(compare and swap)
基礎(chǔ)類型變量自增(i++)是一種常被新手誤以為是原子操作而實(shí)際不是的操作。Java中提供了對應(yīng)的原子操作類來實(shí)現(xiàn)該操作,并保證原子性,其本質(zhì)是利用了CPU級別的CAS指令。由于是CPU級別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小。AtomicInteger使用方法如下:

3.2 如何確保可見性
Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)使用volatile修飾某個變量時,它會保證對該變量的修改會立即被更新到內(nèi)存中,并且將其它線程緩存中對該變量的緩存設(shè)置成無效,因此其它線程需要讀取該值時必須從主內(nèi)存中讀取,從而得到最新的值。
volatile適用場景:volatile適用于不需要保證原子性,但卻需要保證可見性的場景。一種典型的使用場景是用它修飾用于停止線程的狀態(tài)標(biāo)記。如下所示:

在這種實(shí)現(xiàn)方式下,即使其它線程通過調(diào)用stop()方法將isRunning設(shè)置為false,循環(huán)也不一定會立即結(jié)束。可以通過volatile關(guān)鍵字,保證while循環(huán)及時得到isRunning最新的狀態(tài)從而及時停止循環(huán),結(jié)束線程。
3.3 如何確保有序性
上文講過編譯器和處理器對指令進(jìn)行重新排序時,會保證重新排序后的執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致,所以重新排序過程并不會影響單線程程序的執(zhí)行,卻可能影響多線程程序并發(fā)執(zhí)行的正確性。

除了從應(yīng)用層面保證目標(biāo)代碼段執(zhí)行的順序性外,JVM還通過被稱為happens-before原則隱式地保證順序性。兩個操作的執(zhí)行順序只要可以通過happens-before推導(dǎo)出來,則JVM會保證其順序性,反之JVM對其順序性不作任何保證,可對其進(jìn)行任意必要的重新排序以獲取高效率。
happens-before原則(先行發(fā)生原則),如下:

4 關(guān)于線程安全的幾個為什么
1.平時項(xiàng)目中使用鎖和synchronized比較多,而很少使用volatile,難道就沒有保證可見性?

2.鎖和synchronized為何能保證可見性?

3.既然鎖和synchronized即可保證原子性也可保證可見性,為何還需要volatile?

4.既然鎖和synchronized可以保證原子性,為什么還需要AtomicInteger這種的類來保證原子操作?

5.還有沒有別的辦法保證線程安全?

6.synchronized即可修飾非靜態(tài)方式,也可修飾靜態(tài)方法,還可修飾代碼塊,有何區(qū)別?
