第2,3章,介紹了線程安全的最基礎的技術原理,在實際項目中,我們肯定不希望在那個層面來分析每一個對象操作的線程安全性,而是期望使用現有的線程安全組件來構建線程安全的程序。
如何設計一個線程安全的類
如果將一個類的狀態存儲在public static的字段內,那麼保證線程安全是很難的,因爲程序的任何地方都可以自由地修改狀態。良好的封裝限制了狀態的訪問路徑,更容保證線程安全性。
設計一個線程安全類需要考慮一下幾個方面:
- 收集同步需求:識別對象的狀態字段
- 識別對象狀態的正確性定義(還記得狀態不變式和操作後置條件嗎?)
- 設計對象的同步策略
對象的狀態首先是它的字段,如果對象全部由基本字段構成,那麼對象狀態是一個n元組(n是字段個數)。如果對象還包含其他對象的引用,它的狀態還包含被引用對象的字段,以此類推。一個LinkedList的狀態包含它所有節點的狀態。
同步策略定義了對象如何來協調對它的併發訪問,以保證狀態的正確性,狀態的正確性包括不變式和操作後置條件。同步策略指明對象通過一種或多種手段(不可變性,線程封閉,鎖)來保證線程安全,以及哪個鎖保護哪些狀態。
收集同步需求
使一個對象線程安全,就是要確保它的狀態在訪問過程中,保持正確性。對象生命週期內,所有可能的狀態取值,是對象的狀態空間;很顯然,這個空間越小,越容易分析它的線程安全需求。因此儘可能將對象的部分字段設計爲final,能簡化線程安全分析。
對象的不變式能判定對象狀態有效或無效,比如一個計數器Counter,它的狀態空間是從Long.MIN_VALUE到Long.MAX_VALUE,但負值是無效的。對象操作的後置條件,確定了對象狀態有效的狀態遷移,比如Counter目前的狀態是17,那麼increment操作後正確的狀態只有18,而執行K此increment操作的正確後置狀態是(17+k)。
如果對象狀態包含多個字段,比如一個表示範圍的NumberRange類,包含上界和下界兩個變量;不變式要求上界不小於下界,操作後置條件要求上界和下界保持一致性(被同時修改)。
狀態所有權分析
前面實際已經暗示了,對象的狀態實際上是對象所有字段(這裏包括它引用的對象的字段)的一個子集,一些不歸屬它的字段不是對象狀態的一部分。
最典型的是java的集合類,集合類雖然存儲了元素,但元素並不屬於這個集合,而是歸屬於客戶代碼;因而集合類的狀態不包括元素狀態,因而集合的線程安全性也與元素是否線程安全無關。
歸屬與封裝一般是相伴的,一個對象將它的狀態封裝起來,然後擁有它所封裝的狀態。狀態的所有者決定了狀態的同步策略。不過一旦對象將一個可變狀態發佈出去,那麼將失去對它的完全控制,此時只能假定和其他代碼共享狀態所有權。以此推理,對象通過方法參數得到的對象狀態,無法獨佔所有權。
java的垃圾收集,讓我們免於對象歸屬問題所導致的內存錯誤,但並不代表我們不需要考慮這一點。
實例限制
順着狀態所有權的概念,將一個非線程安全對象限制在一個對象實例內部,更容易獲得線程安全性。
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
上面的示例代碼中,mySet字段指向HashSet對象,它是非線程安全的,但是PersonSet將它完全限制在內部,外部只能通過PersonSet的接口來間接訪問它;只要在這些操作上加鎖,就可以爲mySet提供線程安全性。
JDK的Collections.synchronizedList(及類似工廠方法)也是基於這個原理,將非線程安全集合類型提升爲線程安全類型。它使用裝飾者模式,通過一個同步的包裝器來包裝非線程安全集合。
java監視器模式
PersonSet實現線程安全的模式,稱之爲Java監視器模式;它通過java對象內置的監視器鎖來保護內部狀態,這是java特有的一種既簡單又可靠,可以實現實例限制策略的技術方案。
該模式的另一種實現方式是用私有的鎖來代替this鎖:
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod() {
synchronized(myLock) {
// Access or modify the state of widget
}
}
}
上面的實現方式有一個好處,對象用來保護狀態的鎖是私有的,客戶代碼無法使用它,也就無法破壞對象的鎖策略。
委託線程安全
如果對象是由線程安全的組件構成的,那麼該對象是否就是線程安全的呢?換句話說,對象能否將線程安全委託給已經具備線程安全性的組件?這有時候成立,有時候僅僅是實現線程安全的一個良好起點。
案例1. 單狀態字段-成功
看看計數器類Counter的實現,它成功地將線程安全需求委託給了AtomicLong:
public class Counter {
private AtomicLong value = new AtomicLong(0);
public long inrement() {
return value.incrementAndGet();
}
public long get() {
return value.get();
}
}
案例2. 多狀態字段-成功
另個一示例類VisualComponent,它可能發出Key或Mouse相關事件,客戶端可以添加listener來監聽事件:
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
}
VisualComponent有keyListeners和mouseListeners兩個狀態成員,都是線程安全的類型;VisualComponent成功地將線程安全委託給了CopyOnWriteArrayList,它沒有額外的狀態約束條件,因爲keyListeners和mouseListeners互相獨立。
案例3. 多狀態字段-失敗
接下來是一個線程安全委託失敗的示例:
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can’t set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can’t set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
NumberRange是一個表示範圍的類,有下界(lower)和上界(upper)兩個狀態字段,都是類型安全的AtomicInteger對象。爲了滿足(lower<upper)的不變式約束,setLower和setUpper內部都做了檢查。
NumberRange不是線程安全的,因爲setLower和setUpper可能導致無效狀態,非原子化的check-then-act
操作序列存在競爭條件。
實現委託線程安全的條件
一個有多個狀態字段的類,要成功將線程安全委託給它的成員字段,有以下三個條件:
- 每個狀態字段都是線程安全的
- 這些狀態字段相互獨立
- 對象沒有什麼操作過程可能使對象處於無效狀態
能否發佈狀態安全的字段
最後一個小問題,對於已經成功實現線程安全委託的類,我們能否將它的狀態字段發佈出去。
Counter能否將value成員發佈出去?不能,Counter的約束條件有兩個,計數值大於0,且每次操作增加1;如果發佈出去,是不能保證這兩點的;
VisualComponent能否將keyListeners和mouseListeners發佈出去?可以,客戶端代碼直接操作keyListeners並不會使對象狀態處於無效狀態;
所以一個線程安全的對象要安全地將它的狀態成員發佈出去,需要滿足以下條件:
- 狀態字段是線程安全的;
- 對象對該字段的值沒有任何額外約束;
- 該字段也沒有任何操作可以使對象處於無效狀態。
給線程安全類添加方法
假設現在手頭有一個線程安全的List類,我們想要添加一個putIfAbsent方法,仍然保持線程安全特性,可以考慮以下手段。
手段1:修改類源代碼
如果修改源代碼可行,那肯定可以達到目的,只要我們清楚該類的同步策略,並以一種一致的方式擴展它。
手段2:繼承
@ThreadSafe
public class BetterList <E> extends SafeList<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
如果SafeList採用的是java監視器模式,那麼上面的方案是可行的,但是它是脆弱的,因爲它耦合於SafeList的同步策略。一旦SafeList的未來版本修改了鎖機制,該實現將失效。
手段3:Helper類
@NotThreadSafe
public class ListHelper<E> {
public List<E> list = new SafeList<>();
...
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
上線這個實現將putIfAbsent放在一個helper類裏,並加以同步。但是上面的類不是線程安全的,因爲ListHelper和原List使用的是不同的鎖。
手段4:組合
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
public synchronized void clear() { list.clear(); }
// ... similarly delegate other List methods
}
這是類似Collections.synchronizedList的實現方式,將原list完全封裝,並提供安全同步的接口。這是一種安全的實現方式,只不過可增加一層同步方法對性能稍稍有一些影響。
同步策略文檔
文檔是管理類線程安全性的重要工具,代碼的編寫者應當說明類是否線程安全,以及安全策略,包括每一個lock、每個synchronized關鍵字、volatile關鍵字的意圖;對象如果在運行過程中,執行了某個回調接口,需要說明是否有一把鎖正被持有。
不過很多情況下,類的線程安全文檔缺失的,JDK裏的類以及很多著名第三方庫代碼都不例外。用戶只能通過猜測(或者合理推測)它們的線程安全性。SimpleDateFormat是一個典型的反例,從直覺上看,這個類應該是線程安全的,但是它不是,直到java1.4文檔才說明了這一點。
而JDBC的DatasSource則是一個符合直覺的案例,程序內通常只有一個DatasSource實例,那麼多個線程調用DatasSource.getConnection()是必然會發生的場景,那麼該操作理所當然應該是線程安全的。而Connection(連接對象)的典型使用方式是:需要時獲取,使用完釋放
,那麼它可以是非線程安全的。
總結
如果決定了要設計一個線程安全的可變對象,大體步驟如下:
- 界定對象的狀態範圍,尤其要辨別是否包括依賴的其他對象;
- 識別對象的正確性定義,包括狀態不變式和操作後置條件;
- 然後纔是採用合適的線程安全技術:
- 如果狀態字段不可變,無需施加任何線程同步機制;
- 如果狀態字段獨立,那麼可委託給一個java線程安全組件即可;
- 如果操作過程中,會將對象置於無效狀態,那麼需要加鎖。
- 最後,能否能夠將狀態字段發佈出去,按以下規則判定:
- 字段本身是線程安全的;
- 對象對該字段的值沒有任何額外約束;
- 該字段類型沒有任何操作可以使對象處於無效狀態。