線程安全定義
《Java Concurrency In Practice》一書中定義“線程安全”:當(dāng)多個(gè)線程訪問一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果,那這個(gè)對(duì)象是線程安全的。
這個(gè)定義比較嚴(yán)謹(jǐn),它要求線程安全的代碼都必須具備一個(gè)特征:代碼本身封裝了所有必要的正確性保障手段(如互斥同步等),令調(diào)用者無須關(guān)心多線程的問題,更無須自己采取任何措施來保證多線程的正確調(diào)用。
Java語言中線程安全中,各種操作共享的數(shù)據(jù)分為以下5類:不可變、絕對(duì)線程安全、相對(duì)線程安全、線程兼容和線程對(duì)立。
不可變
不可變的對(duì)象一定是線程安全的,不論對(duì)象的方法實(shí)現(xiàn)還是方法的調(diào)用者,都不需要采取任何線程安全保障措施。“不可變”帶來的安全性是最簡(jiǎn)單和最純粹的。
在Java語言中,如果共享數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型,只要final關(guān)鍵字修飾就可以保證它是不可變的。如果共享對(duì)象是一個(gè)對(duì)象,那就需要保證對(duì)象的行為不會(huì)對(duì)其狀態(tài)產(chǎn)生任何影響才行。
絕對(duì)線程安全
完全滿足上訴線程安全的定義,這個(gè)定義其實(shí)是很嚴(yán)格的,通常需要付出很大的代價(jià)才能達(dá)到,甚至有時(shí)候是不切實(shí)際的代價(jià)。
相對(duì)線程安全
相對(duì)的線程安全就是我們通常意義上所講的線程安全,它需要保證對(duì)這個(gè)對(duì)象單獨(dú)的操作是線程安全的。我們?cè)谡{(diào)用的時(shí)候不需要做額外的保障措施,但是對(duì)于一些特定順序的連續(xù)調(diào)用,就可能需要在調(diào)用端使用額外的同步手段來保證調(diào)用的正確性。在Java語言中,大部分的線程安全類都屬于這種類型,例如Vector、HashTable、Collections的synchronizedCollection()方法包裝的集合等。
線程兼容
線程兼容是指對(duì)象本身并不是線程安全的,但是可以通過在調(diào)用端正確地使用同步手段來保證對(duì)象在并發(fā)環(huán)境中可以安全地使用,我們平常說一個(gè)類不是線程安全的,絕大多數(shù)時(shí)候指的是這種情況。Java API中大部分的類都是屬于線程兼容的,如ArrayList和HashMap等。
線程對(duì)立
線程對(duì)立指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼。由于Java語言天生就具備多線程特性,線程對(duì)立這種排斥多線程的代碼是很少出現(xiàn),而且通常都是有害的,應(yīng)當(dāng)避免。常見的例子有Thread類的suspend()和resume()方法,System.setIn()、System.setOut()和System.runFinalizersOnExit()等。
線程安全的實(shí)現(xiàn)
互斥同步
互斥同步(Mutual Exclusion & Synchronion)是常見的一種并發(fā)正確性保障手段。同步是指在多個(gè)線程并發(fā)訪問共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一時(shí)刻只被一個(gè)(或者是一些,使用信號(hào)量的時(shí)候)線程使用。而互斥是實(shí)現(xiàn)同步的一種手段,臨界區(qū)(Critical Section)、互斥量(Mutex)和信號(hào)量(Semaphore)都是主要的互斥實(shí)現(xiàn)方式。因此這里面,互斥是因,同步是果,互斥是方法,同步是目的。
在Java中,最基本的互斥同步方法就是synchronized關(guān)鍵字,synchronized關(guān)鍵字經(jīng)過編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼都需要一個(gè)reference類型的參數(shù)來明確要鎖定和解鎖的對(duì)象。synchronized同步塊對(duì)同一線程來說是可重入的 ,不會(huì)出現(xiàn)自己把自己鎖死的情況,其次,同步塊在已進(jìn)入線程執(zhí)行完之前,會(huì)阻塞后面其他線程的進(jìn)入。
除了使用synchronized之外,我們還可以使用java.util.concurrent包中的重入鎖(ReentrantLock)來實(shí)現(xiàn)同步。相比于synchronized,ReentrantLock增加了一些高級(jí)功能:
- 等待可中斷是指當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對(duì)處理執(zhí)行非常長(zhǎng)的同步塊很有幫助
- 公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖;而非公平鎖則不保證這一點(diǎn),在鎖被釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖。synchronized是非公平鎖,ReentrantLock默認(rèn)情況下是非公平的,但是可以設(shè)置成公平鎖
- 鎖綁定多個(gè)條件是指一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定多個(gè)Condition對(duì)象,而synchronized中,鎖對(duì)象的wait()和notify()或者notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件,如果要和多于一個(gè)條件關(guān)聯(lián)的時(shí)候,就不得不額外地添加一個(gè)鎖,而ReentrantLock則無須這樣做,只需要多次調(diào)用newCondition()方法即可。
非阻塞同步
互斥同步最主要的問題就是進(jìn)行線程阻塞和喚醒所帶來的性能問題,因此這種同步也稱為阻塞同步(Blocking Synchronization)。從出了問題的方式來說,互斥同步是一種悲觀的并發(fā)策略,總是認(rèn)為要是不做正確的同步措施,就肯定出現(xiàn)問題。隨著硬件指令集的發(fā)展,可以采用基于沖突檢測(cè)的樂觀并發(fā)策略,通俗的將就是先進(jìn)行操作,產(chǎn)生了沖突,那就再采取其他補(bǔ)償措施,這種樂觀的并發(fā)策略許多實(shí)現(xiàn)都不需要把線程掛起,因此這種同步操作稱為非阻塞同步(Non-Blockinig Synchronization)。
無同步方案
要保證線程安全,并不是一定要進(jìn)行同步,兩者沒有因果關(guān)系。同步只是保證共享數(shù)據(jù)爭(zhēng)用時(shí)的正確性的手段,如果一個(gè)方法本來就不涉及共享數(shù)據(jù),那它自然就無須任何同步措施去保證正確性,因此會(huì)有一些代碼天生就是線程安全的。
可重入代碼:這種代碼也叫做純代碼(Pure Code),可以在代碼執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用本身),而在控制權(quán)返回后,原來的程序不會(huì)出現(xiàn)任何錯(cuò)誤。相對(duì)線程安全來說,可重入性是更基本的特性,它可以保證線程安全,即所有的可重入代碼都是線程安全的,但是并不是所有的線程安全代碼都是可重入的??芍厝氪a有一些共同特征,例如不依賴存儲(chǔ)在堆上的數(shù)據(jù)和公用的系統(tǒng)資源,用到的狀態(tài)量都由參數(shù)中傳入、不調(diào)用非可重入的方法等。
線程本地存儲(chǔ)(Thread Local Storage):如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)是否能保證在同一個(gè)線程中執(zhí)行,如果能保證,就可以把共享數(shù)據(jù)的可見范圍限制在同一線程之內(nèi),這樣無須同步也能保證下城之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問題。
符合這種特點(diǎn)的應(yīng)用并不少,典型的就是消費(fèi)隊(duì)列架構(gòu)模式,都會(huì)將產(chǎn)品的消費(fèi)過程盡量在一個(gè)線程中消費(fèi)完。Java語言中,如果一個(gè)變量要被多線程訪問,可以使用volatile關(guān)鍵字聲明為“易變的”:如果一個(gè)變量要被某個(gè)線程獨(dú)享,可以通過java.lang.ThreadLocal類來實(shí)現(xiàn)線程本地存儲(chǔ)的功能。每個(gè)線程的Thread對(duì)象都有一個(gè)ThreadLocalMap對(duì)象,這個(gè)對(duì)象存儲(chǔ)了一組以ThreadLocal.threadLocalHashCode為鍵,以本地線程變量為值的K-V值對(duì),ThreadLocal對(duì)象就是當(dāng)前線程的ThreadLocalMap的訪問入口,每個(gè)ThreadLocal對(duì)象都包含了獨(dú)一無二的threadLocalHashCode值,使用這個(gè)值就可以在線程K-V值對(duì)中找回對(duì)應(yīng)的本地線程變量。
參考資料
- 深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐 第2版