要編寫線程安全的代碼,其核心在于要對狀態(tài)訪問操作進(jìn)行管理,特別是對共享的(Shared)和(且)可變的(Mutable)狀態(tài)的訪問。
對象的狀態(tài)是指存儲(chǔ)在狀態(tài)變量(例如實(shí)例或靜態(tài)域)中的數(shù)據(jù)。對象的狀態(tài)可能包括其他依賴對象的域。在對象的狀態(tài)中包含了任何可能影響其外部可見行為的數(shù)據(jù)。
共享:變量可以由多個(gè)線程同時(shí)訪問
可變:變量的值在其生命周期內(nèi)可以發(fā)生變化
一個(gè)對象是否需要是線程安全的,取決于它是否被多個(gè)線程訪問。要使得對象是線程安全的,需要采用同步機(jī)制來協(xié)同對對象(共享)可變狀態(tài)的訪問。
同步機(jī)制的實(shí)現(xiàn):
- synchronized關(guān)鍵字
- volatile類型的變量
- 顯式鎖(Explicit Lock)
- 原子變量
從一開始就設(shè)計(jì)一個(gè)線程安全的類,要比在以后再將這個(gè)類修改為線程安全的類要容易得多。
訪問某個(gè)變量的代碼越少,就越容易確保對變量的所有訪問都實(shí)現(xiàn)正確同步。因此,程序狀態(tài)的封裝性越好,就越容易實(shí)現(xiàn)程序的線程安全性。
當(dāng)性能測試結(jié)果和應(yīng)用需求告訴你必須提高性能,以及測量結(jié)果表明這種優(yōu)化在實(shí)際環(huán)境中確實(shí)能夠帶來性能優(yōu)化時(shí),才進(jìn)行優(yōu)化。在編寫并發(fā)代碼時(shí),應(yīng)該始終遵循這個(gè)原則。由于并發(fā)錯(cuò)誤是非常難以重現(xiàn)和調(diào)試的,因此如果只是在某段很少執(zhí)行的代碼路徑上獲得了性能提升,那么很可能被程序運(yùn)行時(shí)存在的失敗風(fēng)險(xiǎn)而抵消。
區(qū)分:
- 線程安全類
- 線程安全程序
- 線程安全性
完全由線程安全類構(gòu)成的程序不一定就是線程安全的,而在線程安全程序中也可以包含非線程安全的類。只有當(dāng)類中只包含自己的狀態(tài)時(shí),線程安全類才是有意義的。線程安全性只與狀態(tài)相關(guān),因此只能應(yīng)用于封裝其狀態(tài)的整個(gè)代碼,這可能是一個(gè)對象,也可能是整個(gè)程序。
2.1 什么是線程安全性
單線程的正確性:(近似)所見即所知
線程安全性:當(dāng)多個(gè)線程訪問某個(gè)類時(shí),這個(gè)類始終能表現(xiàn)出正確的行為,那么就稱這個(gè)類是線程安全的。
在線程安全類中封裝了必要的同步機(jī)制,因此客戶端無須進(jìn)一步采取同步措施。
無狀態(tài)對象(即沒有實(shí)例變量的對象)一定是線程安全的。
2.2 原子性
競爭條件(Race Condition):由于不恰當(dāng)?shù)膱?zhí)行時(shí)序而出現(xiàn)不正確的結(jié)果
2.2.1 競爭條件
最常見的競爭條件類型是“先檢查后執(zhí)行(Check-Then-Act)”操作,即通過一個(gè)可能失效的觀測結(jié)果來決定下一步的動(dòng)作——基于一種可能失效的觀察結(jié)果來做出判斷或者執(zhí)行某個(gè)計(jì)算
2.2.2 示例:(延遲初始化)中的競爭條件
eg.非線程安全的單例
2.2.3 復(fù)合操作
原子操作
在java.util.concurrent.atomic包中包含了一些原子變量類,用于實(shí)現(xiàn)在數(shù)值和對象引用上的原子狀態(tài)轉(zhuǎn)換。
當(dāng)在無狀態(tài)的類中添加一個(gè)狀態(tài)時(shí),如果該狀態(tài)完全由線程安全的對象類管理,那么這個(gè)類仍然是線程安全的。**
在實(shí)際情況中,應(yīng)盡可能地使用現(xiàn)有的線程安全對象來管理類的狀態(tài)。與非線程安全的對象相比,判斷線程安全對象的可能狀態(tài)及其狀態(tài)轉(zhuǎn)換情況要更為容易,從而也更容易維護(hù)和驗(yàn)證線程安全性。
2.3 加鎖機(jī)制
當(dāng)在不變性條件中涉及多個(gè)變量時(shí),各個(gè)變量之間并不是彼此獨(dú)立的,而是某個(gè)變量的值會(huì)對其他變量的值產(chǎn)生約束。因此,當(dāng)更新某一個(gè)變量時(shí),需要在同一個(gè)原子操作中對其他變量同時(shí)進(jìn)行更新。
要保持狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量。**
2.3.1 內(nèi)置鎖
內(nèi)置的支持原子性的鎖機(jī)制——Synchronized Block:
- 鎖的對象引用
- 由這個(gè)鎖保護(hù)的代碼塊
每個(gè)Java對象都可以用做一個(gè)實(shí)現(xiàn)同步的鎖,這些鎖被稱為內(nèi)置鎖(Intrinsic Lock)或監(jiān)視器鎖(Monitor Lock)。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖,并且在退出同步代碼塊時(shí)自動(dòng)釋放鎖,而無論是通過正常的控制路徑退出,還是通過從代碼塊中拋出異常退出,獲得內(nèi)置鎖的唯一途徑就是進(jìn)入由這個(gè)鎖保護(hù)的同步代碼塊或方法。
內(nèi)置鎖相當(dāng)于一種互斥鎖。
2.3.2 重入
如果某個(gè)線程試圖獲得一個(gè)已經(jīng)由它自己持有的鎖,那么這個(gè)請求就會(huì)成功。
重入意味著獲取鎖的操作的粒度是“線程”而不是“調(diào)用”。
重入的一種實(shí)現(xiàn)方法是,為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和一個(gè)所有者線程。
2.4 用鎖來保護(hù)狀態(tài)
如果用同步來協(xié)調(diào)對某個(gè)變量的訪問,那么在訪問這個(gè)變量的所有位置上都需要使用同步。而且,當(dāng)使用鎖來協(xié)調(diào)對某個(gè)變量的訪問時(shí),在訪問變量的所有位置上都要使用同一個(gè)鎖。**
當(dāng)獲取與對象關(guān)聯(lián)的鎖時(shí),并不能阻止其他線程訪問該對象,某個(gè)對象在獲得對象的鎖之后,只能阻止其他線程獲得同一個(gè)鎖。之所以每個(gè)對象都有一個(gè)內(nèi)置鎖,只是為了免去顯式地創(chuàng)建鎖對象。
一種常見的加鎖約定是,將所有的可變狀態(tài)都封裝在對象內(nèi)部,并通過對象的內(nèi)置鎖對所有訪問可變狀態(tài)的代碼路徑進(jìn)行同步,使得在該對象上不會(huì)發(fā)生并發(fā)訪問。
濫用同步
- 活躍性問題或性能問題
- 只是將每個(gè)方法都作為同步方法,并不足以確保復(fù)合操作都是原子的**
2.5 活躍性與性能
不良并發(fā)(Poor Concurrency)應(yīng)用程序:可同時(shí)調(diào)用的數(shù)據(jù)量,不僅受到可用處理資源的限制,還受到應(yīng)用程序本身結(jié)構(gòu)的限制。
通過縮小同步代碼塊的作用范圍可以做到確保程序的并發(fā)性同時(shí)維護(hù)線程安全性。要確保同步代碼塊不要過小,并且不要將本應(yīng)是原子的操作拆分到多個(gè)同步代碼塊中,應(yīng)該盡量將不影響共享狀態(tài)且執(zhí)行時(shí)間較長的操作從同步代碼塊中分離出去,從而在這些操作的執(zhí)行過程中,其他線程可以訪問共享狀態(tài)。**
eg.


局部變量無需同步,不會(huì)在多個(gè)線程間共享。
對在單個(gè)變量上實(shí)現(xiàn)原子操作來說,原子變量是很有用的,但由于我們已經(jīng)使用了同步代碼塊來構(gòu)造原子操作,而使用兩種不同的同步機(jī)制不僅會(huì)帶來混亂,也不會(huì)在性能或安全性上帶來任何好處,因此在這里不使用原子變量。
在獲取與釋放鎖等操作上都需要一定的開銷,因此如果將同步代碼塊分解得過細(xì),那么通常并不好。
如果持有鎖的時(shí)間過長(執(zhí)行計(jì)算密集的操作或某個(gè)可能阻塞的操作),那么會(huì)帶來活躍性或性能問題。
要判斷同步代碼塊的合理大小,需要在各種設(shè)計(jì)需求之間進(jìn)行權(quán)衡,包括安全性、簡單性和性能**。