從0學習java併發編程實戰-讀書筆記-對象的組合(3)

設計線程安全的類

在設計線程安全類的過程當中,須要包涵如下三個基本要素:java

  • 找出構成對象狀態的全部變量
  • 找出約束狀態變量的不變性條件
  • 創建對象狀態的併發訪問管理策略

同步策略(Synchronization Policy)定義瞭如何在不違背對象不變條件或後驗條件的狀況下對其狀態的訪問操做進行協同。數組

收集同步需求

要確保類的線程安全性,就須要保證它的不變性條件不會在併發訪問的狀況下被破壞。安全

對象和變量都有一個狀態空間,即全部可能的取值。狀態空間越小,就越容易判斷線程的狀態。final類型的域使用的越多,就越能簡化對象可能狀態的分析過程。併發

若是不瞭解對象的不變性條件與後驗條件,那麼就不能確保線程安全性。要知足在狀態變量的有效值狀態轉化上的各類約束條件,就須要藉助原子性與封裝性。

依賴狀態的操做

類的不變性條件與後驗條件約束了對象上有哪些狀態和狀態轉換是有效的。在某些狀況,還包涵一些基於狀態的先驗條件(Precondition),例如不能得到null的引用等。函數

在單線程程序裏,若是某個操做沒法知足先驗條件,就只能失敗。而在併發程序中,先驗條件可能會因爲其餘線程執行的操做而變成真,在併發程序中,要一直等到先驗條件爲真,而後再執行該操做。性能

狀態的全部權

在定義哪些變量將構成對象的狀態時,只考慮對象擁有的數據。若是分配了一個HashMap對象,等於建立了多個對象:this

  • HashMap對象
  • 在HashMap對象中包涵的多個對象
  • 在Map.Entry中可能包涵的內部對象

垃圾回收機制讓咱們避免瞭如何處理全部權的問題。在許多狀況下,全部權和封裝性是互相關聯的:線程

  • 對象封裝它擁有的狀態
  • 對它封裝的狀態擁有全部權

若是發佈了某個可變對象的引用,那麼就再也不擁有獨佔控制權,最可能是共享控制權。對於從構造函數或者從方法中傳遞進來的對象,類一般並不擁有這些對象,除非這些方法是被專門設計爲轉移傳遞進來的對象的全部權(例如同步容器封裝器的工廠方法)。設計

實例封閉

實例封閉指的是:某個對象只能由單個線程訪問。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);
    }
}
  • mySet是個hashset,並非線程安全的,可是因爲mySet被封閉在PersonSet對象中,且能訪問mySet的方法都被同步保護起來
  • 而且mySet由final修飾,因此不須要關心逸出問題。
  • 若是Person類是可變的,那麼在從PersonSet中得到Person時,還須要作額外的同步,要想安全地使用Person對象,最可靠的方法是將Person成爲線程安全的類,固然也可使用鎖來保護Person對象,並確保全部客戶代碼在訪問person的時候都得到了正確的鎖。
封閉機制更易於構造線程安全的類,由於當封閉類的狀態時,在分析類的線程安全性時,就無需檢查整個程序。

Java監視器模式

遵循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);
    }
  • 雖然MutablePoint對象不是線程安全的,可是這個追蹤器類是線程安全的,不管是構造,仍是訪問,都利用了深拷貝來複制正確的值,從而生成了新的對象。
  • 這種方式經過複製可變數據來維護線程安全,在一般狀況下不存在什麼性能問題,可是在車輛容器Map很是大的狀況下,將極大的下降性能。
  • 經過拷貝的方式有個錯誤狀況,就是車輛的位置實際上已經發生了變化,可是返回的信息確實不變的。這種狀況是好是壞,就要取決於用戶的需求。
將多個非線程安全的類組合成爲一個類時,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);
        }
    }
}
  • getLocations返回的是一個不可修改的unmodifiableMap映射,getLocation返回的是一個線程安全的不變對象Point,location和unmodifiableMap 是在構造中經過final域發佈的,因此這些操做不存在併發安全性問題。
  • 可是值得一提的是,setLocation 是能夠線程覆蓋更新的,返回給線程的是當且最新的值,這個和以前的監視器模式呈現相反的效果:即使再也不繼續請求getlocation,僅是觀察所保存的對象,依然能夠得到最新的狀態。(得到的是location的實時只讀拷貝)

獨立的狀態變量

若是咱們將線程安全性委託給多個狀態變量,只要這些狀態變量是各自獨立的,即組合成的類並不會在其保護的多個狀態變量上增長任何不變性條件。例如鼠標事件監聽器和鍵盤事件監聽器之間不存在任何關係,兩者相互獨立,因此能夠將線程安全性委託給這兩個線程安全的監聽器列表。

當委託失效時

固然,大多數組合不會徹底的各自獨立,不存在任何關係:在它們的狀態變量之間存在着某些不變性條件。

若是一個類是由多個獨立且線程安全的狀態變量組成,而且在全部的操做中都不包含無效狀態轉換,那麼能夠將線程安全性委託給底層的狀態變量。

即便類中的各個狀態組成部分都是線程安全的,也不能保證這個類必定是線程安全的。這一觀點很相似於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;
    }
}
  • SafePoint類提供的get方法同時得到x,y的值,並將兩者放在一個組中返回。
  • 若是爲x和y單獨提供get方法,可能會存在在得到兩個不一樣的座標之間,x,y值發生變化,從而致使調用者得到一個safepoint未曾到過的值。
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);
    }
}
  • PublishingVehicleTracker將線程安全性委託給底層的ConcurrentHashMap,map中的對象是線程安全的SafePoint,以此來達到線程安全的目的。
  • PublishingVehicleTracker中經過getLocations得到底層只讀副本,再經過setLocation修改對象狀態,可是卻沒法增長或者刪除車輛。
  • 若是須要對車輛位置進行判斷或者當位置變化時執行一些操做,那麼PublishingVehicleTracker就再也不是線程安全的了。

在現有的線程安全類中增長功能

假如,咱們須要一個鏈表,它須要能提供一個原子的「若沒有就添加的功能(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;
    }
}
  • ImprovedList假設把某個鏈表對象傳給構造函數之後,客戶端不會再直接使用這個對象,而是經過ImprovedList訪問。
  • ImprovedList經過自身的內置鎖增長了一層額外的加鎖,它並不關心底層的List是不是線程安全的,即便List不是線程安全的,或List修改了它的同步策略,ImprovedList也會提供一套自有的加鎖機制來實現線程安全性。雖然額外的同步層會形成性能的損失,可是代碼更加的健壯。
  • 事實上,咱們使用了java監視器模式來封裝現有的list,而且只要在類中擁有指向底層List的惟一外部引用,就能保證線程安全性。
相關文章
相關標籤/搜索