《JAVA併發程式設計實戰》物件的組合

sayWhat_sayHello發表於2018-10-23

設計執行緒安全的類

在設計執行緒安全類的過程中,需要包含以下三個基本要素:

  1. 找出構成物件狀態的所有變數
  2. 找出約束狀態變數的不變性條件
  3. 建立物件狀態的併發訪問管理策略

找出構成物件狀態的所有變數

分析物件的狀態,首先從物件的域開始:

  • 如果物件所有域都是基本型別的變數,那麼這些域構成物件的全部狀態。對於含有n個基本型別域的物件,其狀態就是這些域構成的n元組。
  • 如果在物件的域中引用了其他物件,那麼該物件的狀態將包含被引用物件的域。

示例

public class Person{
    private int id;
}

這個物件的域為id,這個域就是Person物件的全部狀態。

public class Coordinate{
    private int x;
    private int y;
}

這個物件有兩個域x,y;Coordinate物件的狀態為二元組(x,y);

public class Diary{
    private Person person;
}

這個物件有兩個域person,這個域都是引用型別。所以狀態包含person物件裡的域id.

找出約束狀態變數的不變性條件

利用不變性條件判斷狀態是否有效,例如整型int型別,狀態空間為Integer.MIN_VALUE~Integer.MAX_VALUE

後驗條件判斷遷移是否有效。如果在某個操作中存在無效的狀態轉換,那麼該操作必須是原子的。

例項封閉

如果某物件不是執行緒安全的,一般可以通過兩種技術使其在多執行緒程式中安全的使用:

  • 確保該物件只能由單個執行緒訪問(執行緒封閉)
  • 通過一個鎖來保護該物件的所有訪問。
public class PersonSet{
    private final Set<Person> mySet = new HashSet<>();
    
    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }
    
    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }
}

這個類是執行緒安全的,雖然HashSet是非執行緒安全的,但是mySet是私有的,且不會被get,因此HashSet是被封閉在PersonSet中的。而且能訪問mySet的addPerson和containsPerson在執行時需要訪問PersonSet的內建鎖,因此PersonSet是個執行緒安全的類。需要注意的是這裡的Person類如果是可變的,那麼在訪問從PersonSet中獲得的Person物件時,還需要額外的同步。

java監視器模式

遵循java監視器模式的物件會把所有可變狀態都封裝起來,並由物件的內建鎖保護。

私有的鎖物件:

//執行緒安全
public class PrivateLock{
    private Object myLock = new Object();
    Widget widget;
    void someMethod() {
        synchronized(myLock) {
            // 訪問或修改Widget的狀態
        }
    }
}

使用私有的鎖物件而不是物件的內建鎖,有以下優點:

  • 私有的鎖物件可以將鎖封裝起來,使客戶程式碼無法獲得鎖,但客戶程式碼可以通過公有方法來訪問鎖,以便參與到它的同步策略中。

示例:車輛追蹤

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 IllegalArgumentException("No such ID: "+ id);
        }
        loc.x = x;
        loc.y = y;
    }
    
    private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m) {
        Map<String,MutablePoint> res = new HashMap<String,MutablePoint>();
        for(String id : m.keySet()) {
            res.put(id,new MutablePoint(m.get(id)));
        }
        return Collections.unmodifiableMap(res);
    }
    
    // 這個類非執行緒安全
    public class MutablePoint {
        public int x,y;
        public MutablePoint() {
            x = 0;
            y = 0;
        }
        public MutablePoint(MutablePoint p) {
            this.x = p.x;
            this.y = p.y;
        }
    }
}

執行緒安全性的委託

public class CountingFactorizer{
    public AtomicLong value;
}

對於這樣一個類,由於AtomicLong是執行緒安全的,而且CountingFactorizer只包含value一個狀態,所以CountingFactorizer是執行緒安全的。我們將這個過程稱為執行緒安全性的委託,CountingFactorizer將它的執行緒安全性委託給AtomicLong保證。

示例:基於委託的車輛追蹤器

public class DelegatingVehicleTracker {
    private final ConcurrentMap<String,Point> locations;
    private final Map<String,Point> unmodifiableMap;
    
    public DelegatingVehicleTracker(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);
        }
    }
}

class Point {
    public final int x,y;
    
    public Point(int x,int y) {
        this.x = x;
        this.y = y;
    }
}

如果使用最初的MutablePoint而不是Point類,就會破壞封裝性,因為getLocations會釋出一個指向可變狀態的引用,而這個引用不是執行緒安全的。

委託給多個獨立的狀態變數

前面的委託都是針對單個狀態變數,我們還可以將執行緒安全性委託給多個狀態變數。

public class VisualComponent {
    private final List<KeyBoardListener> keyBoardListener = new CopyOnWriteArrayList<KeyBoardListener>();
    private final List<MouseListener> keyBoardListener = new CopyOnWriteArrayList<MouseListener>();
    
    public void addKeyBoardListener(KeyBoardListener listener) {
        keyBoardListener.add(listener);
    }
    public void addMouseListener(MouseListener listener) {
        MouseListener.add(listener);
    }
    public void removeKeyBoardListener(KeyBoardListener listener) {
        keyBoardListener.remove(listener);
    }
    public void removeMouseListener(MouseListener listener) {
        MouseListener.remove(listener);
    }
}

委託失效

當有約束條件時,委託容易失效

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("can't set low to "+ i + " > upper");
        }
        lower.set(i);
    }
    
    public void setUpper(int i) {
        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不是執行緒安全的,沒有維持對下界和上界進行約束的不變性條件。

setLower 和 setUpper都嘗試維持不變性,但是都沒有做到,因為他們是採用“先檢查後執行”的操作,但沒有使用足夠的加鎖機制保證這些操作的原子性。如果一個執行緒呼叫setLower(5),另一個執行緒呼叫setUpper(4),如果執行時序錯誤,那麼兩個方法同時通過檢查進行設定就會導致upper<lower。不符合約束。

如果一個類是由多個獨立且執行緒安全的狀態變數組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將執行緒安全性委託給底層的狀態變數。

釋出底層的狀態變數

根據類對底層狀態變數施加的不變性條件,我們才可以釋出這些變數從而使其他類能修改他們。

如果一個狀態變數是執行緒安全的,並且沒有任何不變性條件約束它的值,在變數的操作上也不存在任何不允許的狀態轉換,那麼就可以安全的釋出這個變數。

示例:釋出狀態的車輛追蹤器

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<String,SafePoint>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(this.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("invalid vehicle name:  " + id);
        }
        locations.get(id).set(x,y);
    }
}

class SafePoint {
    private int x,y;
    private SafePoint(int[] a){
        this(a[0],a[1]);
    }
    
    public 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(SafePoint p)獲取p的拷貝,方法體內不直接呼叫SafePoint(int x,int y)的原因是因為避免產生競態條件,私有建構函式可以避免這種競態條件。(私有建構函式捕獲模式)

PublishingVehicleTracker將其執行緒安全性委託給底層的ConcurrentHashMap,不同的是Map中的元素是執行緒安全且可變的Safeoint,而並非不可變的。getLocation返回底層Map物件的一個不可變副本。呼叫者可以修改Map中的SafePoint值改變車輛的位置。

在現有的執行緒安全類中新增功能

假設需要一個執行緒安全的連結串列,它需要提供一個原子的“若沒有則新增”操作。

因為需要的是一個執行緒安全的類,那麼這種“先查詢後執行”的操作就要是原子的.

最安全的方式是修改原始的類,但這通常無法做到。另一個方法就是擴充這個類,但是並非所有的類都像Vector這樣將狀態像子類公開。

public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if(absent) {
            add(x);
        } 
        return absent;
    }
}

客戶端加鎖機制

對於由Collections.synchronizedList封裝的ArrayList,在原始類中新增一個方法或者對類進行擴充套件都不行,第三種策略是擴充套件類的功能,但不是擴充套件類本身,而是將擴充套件程式碼放入一個“輔助類”中。

一個錯誤的做法:

public class ListHelper<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;
    }
}

錯誤的原因是synchronized沒有鎖住list,在執行putIfAbsent時可能list會被另外一個執行緒修改。

如果想要正確執行該方法,必須使List在實現客戶端加鎖或外部加鎖時使用同一個鎖

客戶端加鎖是值,對使用某個物件X的客戶端程式碼,使用X本身用於保護其狀態的鎖來保護這段客戶程式碼。在這裡X指的是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;    
        }
        
    }
}

通過新增一個原子操作擴充套件類是脆弱的,因為它將類的加鎖程式碼分佈到多個類中。客戶端加鎖卻更加脆弱因為它將類X的加鎖程式碼放到和X完全無關的其他類中。

組合

組合是一種更好的方法。

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();
    }
}

通過自身的內建鎖增加了一層額外的加鎖。我們使用了java監視器模式來封裝現有List,並且只要在類中擁有指向底部List的唯一外部引用,就能確保執行緒安全性。

將同步策略文件化

在文件中說明客戶程式碼需要了解的執行緒安全性保證,以及程式碼維護人員需要了解的同步策略。

在設計同步策略時需要考慮多個方面:例如,將哪些變數宣告為volatile型別,哪些變數用鎖來保護,哪些鎖保護哪些變數,哪些變數必須是不可變的或者被封閉線上程中的,哪些操作必須是原子操作等。

相關文章