在寫Java程序的時候,何時需要進行并發控制,關鍵在于判斷這段程序或這個類是否是線程安全的。
當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替執行,并且不需要額外的同步,這個類的行為仍然是正確的,那么稱這個類是線程安全的。我們設計類就是要在有潛在并發問題存在情況下,設計線程安全的類。線程安全的類可以通過以下手段來滿足:
- 不跨線程共享變量
- 使狀態變量為不可變的
- 在任何訪問狀態變量的時候使用同步。
- 每個共享的可變變量都需要由唯一一個確定的鎖保護。
滿足線程安全的一些思路
1)從源頭避免并發問題
很多開發者一想到有并發的可能就通過底層技術來解決問題,其實往往可以通過上層的架構設計和業務分析來避免并發場景。比如我們需要用多線程或分布式集群來計算一堆客戶的相關統計值,由于客戶的統計值是共享數據,因此會有并發潛在可能。但從業務上我們可以分析出客戶與客戶之間數據是不共享的,因此可以設計一個規則來保證一個客戶的計算工作和數據訪問只會被一個線程或一臺工作機完成,而不是把一個客戶的計算工作分配給多個線程去完成。這種規則很容易設計。當你從源頭就避免了并發問題的可能,下面的工作就完全可以不用擔心線程安全問題。
2)無狀態就是線程安全
多線程編程或者分布式編程最忌諱有狀態,一有狀態就不但限制了其橫向擴展能力,也是產生并發問題的起源。當你設計的類是無狀態的,那么它永遠都是線程安全的。因此在設計階段需要考慮如何用無狀態的類來滿足你的業務需求
3)分清原子性操作和復合操作
所謂原子性,是說一個操作不會被其他線程打斷,能保證其從開始到結束獨享資源連續執行完這一操作。如果所有程序塊都是原子性的,那么就不存在任何并發問題。而很多看上去像是原子性的操作正式并發問題高災區。比如所熟知的計數器(count++)和check-then-act,這些都是很容易被忽視的,例如大家所常用的惰性初始化模式,以下代碼就不是線程安全的:
- @NotThreadSafe
- public class LazyInitRace {
- private ExpensiveObject instance = null;
- public ExpensiveObject getInstance() {
- if (instance == null)
- instance = new ExpensiveObject();
- return instance;
- }
- }
這段代碼具體問題在于沒有認識到if(instance==null)和instance = new ExpensiveObject();是兩條語句,放在一起就不是原子性的,就有可能當一個線程執行完if(instance==null)后會被中斷,另一個線程也去執行if(instance==null),這次兩個線程都會執行后面的instance = new ExpensiveObject();這也是這個程序所不希望發生的。
雖然check-then-act從表面上看很簡單,但卻普遍存在與我們日常的開發中,特別是在數據庫存取這一塊。比如我們需要在數據庫里存一個客戶的統計值,當統計值不存在時初始化,當存在時就去更新。如果不把這組邏輯設計為原子性的就很有可能產生出兩條這個客戶的統計值。
在單機環境下處理這個問題還算容易,通過鎖或者同步來把這組復合操作變為原子操作,但在分布式環境下就不適用了。一般情況下是通過在數據庫端做文章,比如通過唯一性索引或者悲觀鎖來保障其數據一致性。當然任何方案都是有代價的,這就需要具體情況下來權衡。
另外,java1.5以后提供了一套提供原子性操作的類,有興趣的可以研究一下它是如何在軟件層面保證原子性的。
4)鎖的合理使用
大家都知道可以用鎖來解決并發問題,但在具體使用上還有很多講究,比如:
- 每個共享的可變變量都需要由一個個確定的鎖保護。
- 一旦使用了鎖,就意味著這段代碼的執行就喪失了操作系統多道程序的特性,會在一定程度上影響性能
- 鎖不能解決在分布式環境共享變量的并發問題