設計線程安全的類--對象的組合
通過一些組合模式能夠使一個類更容易成為線程安全的,並且易於維護。避免復雜的內存分析來確保線程是安全的。
設計一個線程安全的類要報案下面三個要素:
1、找出構成對象的狀態的所有變量。
對象的所有域構成了對象的狀態。如果對象的域是基本變量構成,那麽這些域構成了對象的全部狀態。如果對象的域中引用了其他對象,那麽對象的狀態也包含其引用對象的域。如ArrayList的狀態就包含其所有節點對象的狀態。
2、找出約束狀態變量的不變性條件
3、建立對象狀態的並發訪問策略。
將不變性,線程封閉,加鎖機制等結合起來維護線程的安全性。
收集同步需求
要確保線程安全,就需要確保不變性條件不會再並發訪問中被破壞。對象和變量都有一個狀態空間,即所有可能的取值。狀態空間越小越容易判斷線程狀態。
public final class Counter{ private long value = 0; public synchronized long getValue(){ return value; } public synchronized long increment(){ if(value == Long.MAX_VALUE) throw new IllegalStateException(); return ++value; } }
許多類中都定義了不可變條件,用於判斷狀態是否有效。如Counter中的value是long類型的變量,取值範圍是Long.MIN_VALUE到Long.MAX_VALUE,但是還有一個限制,就是value不能是負值。
類中也存在中後驗條件類判斷狀態的遷移是否有效。如counter的當前狀態是17,那麽下一個有效狀態就是18。當下一個狀態依賴當前狀態,那麽這個操作就是符合操作。並非soy狀態轉換操作都有後驗條件,比如更新溫度時,並不依賴前一個結果。
由於不變性條件和後驗條件在狀態和狀態轉換上施加了約束,因此需要額外的同步和封裝,否則客戶端代碼可能是對象處於無效狀態。如果存在無效的狀態轉換,那麽必須是原子操作。
對於包含多個狀態變量的不變性條件,需要在原子操作中進行讀取和更新,不能首先更新一個變量然後釋放鎖,然後更新其他變量。因為釋放鎖後,可能是對象處於無效狀態。
要確保對象的線程安全,必須要了解對象的不變性條件和後驗條件,需要借助原子性和封裝性。
先驗條件(Pre-condition),後驗條件(Post-condition),不變性條件(Invariant)含義如下:
- Pre-conditions are the things that must be true before a method is called. The method tells clients "this is what I expect from you".
- Post-conditions are the things that must be true after the method is complete. The method tells clients "this is what I promise to do for you".
- Invariants are the things that are always true and won‘t change. The method tells clients "if this was true before you called me, I promise it‘ll still be true when I‘m done".
依賴狀態的操作
某些對象的方法中包含一些基於狀態的先驗條件,不能從一個空隊列中移除一個元素。在單線程中,無法滿足先驗條件,操作會失敗。但是在並發程序中,因為其他線程的操作使得原本不滿足先驗條件的操作成功執行。
實例封閉
對於一個非線程安全的對象可以通過一些技術使其在多線程中安全使用。如,確保單線程訪問該對象,或者通過鎖來保護該對象。
通過封裝可以簡化線程安全類的實現過程,它提供了一種實例封閉機制。當一個對象被封裝到另一個對象中時,所有訪問該對象的代碼都是已知的,相比被整個程序訪問的對象,被封裝的對象更容易分析。被封閉的對象一定不能超出既定的作用域。
將數據封閉在對象內部,可以將數據的訪問限制在對象方法上,從而更容易確保線程訪問數據時持有正確的鎖。
public class PersonSet{ private final Set<Persion> mySet = new HashSet<>(); public synchronized long addPersion(Persion p){ mySet.add(p); } public synchronized boolean containsPersion(Persion p){ return mySet.contains(p); } }
HashSet並非線程安全的,但是HashSet被封閉在PersionSet中,唯一能訪問mySet的代碼都由鎖保護的。因此PersionSet的線程安全的。
本例中並沒有假設Persion的線程安全性。如果Persion是可變的,那麽訪問persion還需要額外的同步。
JAVA的類庫中也有很多類似的線程封閉的類,如Collections.synchronizedList及其類似方法,這些類的唯一用途就是將非線程安全的類轉換成線程安全的類。
Java監視器模式
java的內置鎖被稱為監視器鎖或監視器,將所有可變對象封裝起來,並有對象自己的內置鎖保護。屬於實例封閉。前面的Counter示例就是這種模式。也可以通過私有鎖來保護對象,可以避免客戶端代碼獲取鎖,產生死鎖問題,而且只需檢查單個類就可以驗證鎖是否正確使用。
示例:車輛追蹤
class MutablePoint{ public int x,y; public MutablePoint(){ x=0; y=0; } public MutablePoint(MutablePoint point){ this.x = point.x; this.y = point.y; } } public class MonitorVehicleTracker{ private final Map<String, MutablePoint> locations; public MonitorVehicleTracker(Map<String, MutablePoint> locations){ this.locations = locations; } public synchronized Map<String, MutablePoint> getLocations(){ return deepCopy(locations); } public synchronized MutablePoint getLocation(String id){ MutablePoint loc = locations.get(id); return loc == null?null:new MutablePoint(loc); } public synchronized void setLocation(String id, int x, int y){ MutablePoint loc = locations.get(id); if(loc == null) throw new IllegalStateException(); loc.x = x; loc.y = y; } private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m){ Map<String, MutablePoint> locs = new HashMap<>(); for(String id:m.keySet()){ MutablePoint loc = new MutablePoint(m.get(id)); locs.put(id, loc); } return Collections.unmodifiableMap(locs); } }
假設每輛車都有一個String對象來標記,同時擁有一個位置坐標(x,y)。通過一個線程讀取位置,將其顯示出來,vehicles.getLocations()
其他線程負責更新車輛的位置。vehicles.setLocation(id, x, y);
由於存在並發訪問,必須是線程安全的,因此使用了監視器模式,確保了線程的安全。盡管MutablePoint不是線程安全的,但是可變的Point並沒有被發布。當返回車輛位置時,通過deepCopy方法來復制當前的位置。因此MonitorVehicleTracker是線程安全的。通過復制可變數據類維持線程安全。可能存在一些問題,如性能問題,不能實時反映車輛位置,因為返回的是快照。
線程安全性的委托
如果類中的各個組件都是線程安全的,那麽是否還需要額外的線程安全層?需要看情況。在某些情況下,通過線程安全類組合而成的類是線程安全的,稱之為線程安全性的委托。
將車輛追蹤器的實例改變下,代碼如下:
class Point{ public final int x,y; public Point(int x, int y){ this.x=x; this.y=y; } } public class MonitorVehicleTracker{ private final ConcurrentHashMap<String, Point> locations; private final Map<String, Point> unModifiableMap; public MonitorVehicleTracker(Map<String, Point> locations){ this.locations = new ConcurrentHashMap<>(locations); unModifiableMap = Collections.unmodifiableMap(this.locations); } public Map<String, Point> getLocations(){ return unModifiableMap; } public void setLocation(String id, int x, int y){ if(locations.replace(id, new Point(x, y)) == null) throw new IllegalStateException(); } }
我們只是將最初的可變MutablePoint類變成不可變的Poient,不可變的值可以自由的分享和發布,因此返回的locattion不需要復制。使用了線程安全的ConcurrentHashMap來管理,因此沒有使用顯示的同步,同時確保了線程安全。將線程安全委托給ConcurrentHashMap。
委托給獨立的狀態變量
上面我們都是委托給單個線程安全的狀態變量。我們也可以委托給多個狀態變量,但是這些變量必須是彼此獨立的,即組合後的類在多個狀態變量上沒有任何不變性條件。
委托失效
大多數組合對象存在著某些不變性條件。會導致委托失效,非線程安全。
public class NumberRange{ //lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i){ if(i > upper.get()) throw new IllegalArgumentException(); lower.set(i); } public void setUpper(int i){ if(i < lower.get()) throw new IllegalArgumentException(); upper.set(i); } public boolean isInRange(int i){ return (i >= lower.get()) && i <= upper.get(); } }
NumberRange不是線程安全的,因為進行了先檢查後執行操作,並且這個操作不是原子性的,破壞了上下界進行約束的不變性條件。setLower和setUper都嘗試維持不變條件,但是失敗了。我們可以通過加鎖機制來維護不變性條件來確保線程安全性。因此類似的符合操作,僅靠委托無法實現線程安全。
如果一個狀態變量是線程安全的,並且沒有任何不變性條件來約束,也不存在無效的狀態轉換,n那麽就可以安全的發布這個變量。
在現有的線程安全類中添加功能
對一個線程安全的類添加原子操作,但是,這通常做不到,因為無法修改源代碼。我們可以擴展這個類,例如BetterVector對Vector進行了擴展,添加一個原子方法,putIfAbsent。
public class BetterVertor<E> extends Vector<E>{ public synchronized boolean putIfAbsent(E x){ boolean absent = !contains(x); if(absent) add(x); return absent; } }
上述示例之所以線程安全,是因為Vector將狀態向子類公開,並且規範中定義了同步策略。
客戶端加鎖機制
我們可以用第三種方式來在線程安全類中添加功能,擴展類的功能,並不擴展類的本身,將擴展代碼放入輔助類中。
public class ListHepler<E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent) list.add(x); return absent; } }
這個類看起來是線程安全的,畢竟使用了同步方法。然而這並不是線程安全的,問題在於在錯誤的鎖上進行了同步。因為不管list使用哪個鎖來保護狀態,但肯定不是ListHelper上的鎖。意味著putIfAbsent相對於List的其他操作並不是原子的。
要想使這個方法正確執行,必須是List在實現客戶端加鎖時使用同一個鎖。下面是正確的示例。
public class ListHepler<E> { public List<E> list = Collections.synchronizedList(new ArrayList<E>()); public boolean putIfAbsent(E x){ synchronized(list){ boolean absent = !list.contains(x); if(absent) list.add(x); return absent; } } }
組合
還有一種方式來添加原子操作:組合。
public class ImprovedList<E> { private final List<E> list; public ImprovedList(List<E> list){ this.list = list; } public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent) list.add(x); return absent; }
pubic sunchronized void otherMethod(){
...
}
}
客戶端並不會直接使用list這個對象,因此並不關心list是否是線程安全的,ImprovedList通過自身內置鎖增加了一層額外的鎖。事實上,我們使用了監視器模式封裝了現有的list。只要確保客戶端代碼不直接使用list就能確保線程安全性。
設計線程安全的類--對象的組合