在設計線程安全類的過程當中,須要包涵如下三個基本要素:java
同步策略(Synchronization Policy)
定義瞭如何在不違背對象不變條件或後驗條件的狀況下對其狀態的訪問操做進行協同。數組
要確保類的線程安全性,就須要保證它的不變性條件不會在併發訪問的狀況下被破壞。安全
對象和變量都有一個狀態空間,即全部可能的取值。狀態空間越小,就越容易判斷線程的狀態。final類型的域使用的越多,就越能簡化對象可能狀態的分析過程。併發
若是不瞭解對象的不變性條件與後驗條件,那麼就不能確保線程安全性。要知足在狀態變量的有效值狀態轉化上的各類約束條件,就須要藉助原子性與封裝性。
類的不變性條件與後驗條件約束了對象上有哪些狀態和狀態轉換是有效的。在某些狀況,還包涵一些基於狀態的先驗條件(Precondition)
,例如不能得到null的引用等。函數
在單線程程序裏,若是某個操做沒法知足先驗條件,就只能失敗。而在併發程序中,先驗條件可能會因爲其餘線程執行的操做而變成真,在併發程序中,要一直等到先驗條件爲真,而後再執行該操做。性能
在定義哪些變量將構成對象的狀態時,只考慮對象擁有的數據。若是分配了一個HashMap對象,等於建立了多個對象:this
垃圾回收機制讓咱們避免瞭如何處理全部權的問題。在許多狀況下,全部權和封裝性是互相關聯的:線程
若是發佈了某個可變對象的引用,那麼就再也不擁有獨佔控制權,最可能是共享控制權。對於從構造函數或者從方法中傳遞進來的對象,類一般並不擁有這些對象,除非這些方法是被專門設計爲轉移傳遞進來的對象的全部權(例如同步容器封裝器的工廠方法)。設計
實例封閉指的是:某個對象只能由單個線程訪問。code
將數據封裝在對象內部,能夠將訪問限制在對象的方法上,從而更容易確保線程在訪問數據時總能持有正確的鎖
被封閉的對象必定不能超過它們既定的做用域。對象能夠封閉在類的一個實例上(例如做爲類的私有成員),或者封閉在某個做用域內(例如做爲一個局部變量),再或者封閉在線程內(例如在同一線程內,將某個對象從一個方法傳遞到另外一個方法)。
public class PersonSet { private final Set<Person> mySet = new HashSet<>(); public synchronized void addPerson(Person person){ mySet.add(person); } public synchronized boolean containsPerson(Person person){ return mySet.contains(person); } }
封閉機制更易於構造線程安全的類,由於當封閉類的狀態時,在分析類的線程安全性時,就無需檢查整個程序。
遵循java監視器模式的對象會把全部可變狀態都封裝起來,並由對象本身的內置鎖來保護。
在許多類中都使用了java監視器模式,例如Vector和Hashtable。java監視器模式的主要優點在於其的簡單性。
使用私有的鎖對象而不是對象的內置鎖(或任何其餘能夠經過公有方式訪問的鎖),有許多優勢:
public class MonitorVehicleTracker { private final Map<String, MutablePoint> locations; public MonitorVehicleTracker(Map<String, MutablePoint> locations) { this.locations = deepCopy(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 IllegalArgumentException("no such id:"+id); } loc.x = x; loc.y = y; } private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) { Map<String, MutablePoint> result = new HashMap<>(); for (String id : m.keySet()) { result.put(id, new MutablePoint(m.get(id))); } return Collections.unmodifiableMap(result); }
將多個非線程安全的類組合成爲一個類時,java監視器模式是很是有用的。
首先,MutablePoint須要改成線程安全的Point
/** * 經過不變性保證線程安全 */ public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
因爲Point是不可變的,因此是線程安全的,final類型的值能夠被自由的共享和發佈。
public class DelegationVehicleTracker { private final ConcurrentMap<String, Point> locations; private final Map<String, Point> unmodifiableMap; public DelegationVehicleTracker(Map<String, Point> points) { locations = new ConcurrentHashMap<String, Point>(points); unmodifiableMap = Collections.unmodifiableMap(locations); } public Map<String, Point> getLocations() { return unmodifiableMap; } public Point getLocation(String id) { return locations.get(id); } public void setLocation(String id, int x, int y) { if(locations.replace(id,new Point(x,y)) == null){ throw new IllegalArgumentException("invalid vehicle name :"+id); } } }
若是咱們將線程安全性委託給多個狀態變量,只要這些狀態變量是各自獨立的,即組合成的類並不會在其保護的多個狀態變量上增長任何不變性條件。例如鼠標事件監聽器和鍵盤事件監聽器之間不存在任何關係,兩者相互獨立,因此能夠將線程安全性委託給這兩個線程安全的監聽器列表。
固然,大多數組合不會徹底的各自獨立,不存在任何關係:在它們的狀態變量之間存在着某些不變性條件。
若是一個類是由多個獨立且線程安全的狀態變量組成,而且在全部的操做中都不包含無效狀態轉換,那麼能夠將線程安全性委託給底層的狀態變量。
即便類中的各個狀態組成部分都是線程安全的,也不能保證這個類必定是線程安全的。
這一觀點很相似於volatile的變量規則:僅當一個變量參與到包含其餘變量的不變性條件時,才能夠聲明爲volatile
當把線程安全性委託給某個對象的底層狀態變量時,在什麼條件下才能夠發佈這些變量從而使其餘類能修改它們?答案仍然取決於在類中對這些變量施加了哪些不變性條件。
若是一個狀態變量是線程安全的,而且沒有任何不變性條件來約束它的值,在變量的操做上也不存在任何不容許的狀態轉換,那麼就能夠安全的發佈這個變量。
public class SafePoint { private int x, y; private SafePoint(int[] a) { this(a[0], a[1]); } private SafePoint(SafePoint p) { this(p.get()); } public SafePoint(int x, int y) { this.x = x; this.y = y; } public synchronized int[] get() { return new int[]{x, y}; } public synchronized void set(int x, int y) { this.x = x; this.y = y; } }
public class PublishingVehicleTracker { private final Map<String, SafePoint> locations; private final Map<String,SafePoint> unmodifiableMap; public PublishingVehicleTracker(Map<String,SafePoint> locations){ this.locations = new ConcurrentHashMap<>(locations); this.unmodifiableMap = Collections.unmodifiableMap(locations); } public Map<String,SafePoint> getLocations(){ return unmodifiableMap; } public SafePoint getLocation(String id){ return locations.get(id); } public void setLocation(String id,int x,int y){ if(!locations.containsKey(id)){ throw new IllegalArgumentException("invaild vehicle name :"+id); } locations.get(id).set(x,y); } }
假如,咱們須要一個鏈表,它須要能提供一個原子的「若沒有就添加的功能(Put-if-Absent)」的操做。同步的List類已經實現了大部分功能,咱們能夠根據它提供的contains和add方法來構造一個「若沒有則添加的操做」。
要想添加一個新的原子操做,最安全的方法就是修改原始的類,但這個一般沒法作到,由於沒法訪問或修改類的源代碼。要想修改原始類,須要瞭解類的同步策略。這樣增長的代碼才能和原有的設計保持一致。
還有一種方法是拓展這個類,假定這個類在設計的時候考慮了可拓展性,經過繼承該類,添加一個新方法putIfAbsent。
public class BetterVector<E> extends Vector<E>{ public synchronized boolean putIfAbsent(E x){ boolean absent = !contains(x); if(absent){ add(x); } return absent; } }
拓展的代碼更加脆弱,若是底層的類改變了同步策略並選擇了不一樣的鎖保護它的狀態,那麼子類會被破壞,由於在同步策改變後它沒法再使用正確的鎖來控制對基類狀態的併發訪問(Vector的規範中定義了它的同步策略,因此BetterVector不存在這個問題)。
對於由Collections.synchronizedList封裝的ArrayList,這兩種方法在原始類或者對類進行拓展都行不通,由於客戶代碼並不知道在同步封裝器工廠方法中返回的List對象類型。
第三種策略是拓展類的功能,但並非拓展類自己,而是將拓展代碼放入一個輔助類
中。
public class ListHelper<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; } } }
將list對象自己做爲鎖,能夠保證與list對象的其餘操做都是原子的。
經過一個原子操做來拓展類是很脆弱的,由於它將類的加鎖代碼分佈到多個類中。
爲現有的類添加一個原子操做,更好的方法就是:組合(Composition)
public class ImprovedList<T> implements List<T> { private final List<T> list; public ImproveList(List<T> list){ this.list = list; } public synchronized boolean putIfAbsent(T x){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } }