3.多線程之對象的組合

一.設計線程安全的類

  分析對象的域,若是對象中全部的域都是基本類型的變量,那麼這些域將構成對象的所有狀態。數組

1.收集同步需求

  一個類包含兩個狀態變量,分別表示範圍的上界和下屆。這些變量必須遵循的約束是,下界值應該小於或等於上界值。相似於這種包含多個變量的不變性條件將帶來原子性需求:這些相關的變量必須在單個原子操做中進行讀取或更新。不能首先更新一個變量,而後釋放鎖並再次得到鎖,而後再更新其餘的變量。由於釋放鎖後,可能會使對象處於無效狀態。若是在一個不變性條件中包含多個變量,那麼在執行任何訪問相關變量的操做時,都必須持有保護這些變量的鎖。安全

2.依賴狀態的操做

  類的不變性條件與後驗條件約束了在對象上有哪些狀態和狀態轉換是有效的。在某些對象的方法中還包含一些基於狀態的先驗條件( Precondition)。例如,不能從空隊列中移除一個元素,在刪除元素前,隊列必須處於「非空的」狀態。若是在某個操做中包含有基於狀態的先驗條件,那麼這個操做就稱爲依賴狀態的操做。函數

 

二.實例封閉

  將數據封裝在對象內部,能夠將數據的訪問限制在對象的方法上,從而更容易確保?線程在訪問數據時總能持有正確的鎖。性能

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

    interface Person {
    }
}

上面PersonSet說明了如何經過封閉與加鎖等機制使一個類成爲線程安全的(即便這個類的狀態變量並非線程安全的)。PersonSet的狀態由HashSet來管理的,而HashSet並不是線程安全的。但因爲mySet是私有的而且不會逸出,所以HashSet被封閉在PersonSet 中。惟一能訪問mySet的代碼路徑是addPerson與containsPerson,在執行它們時都要得到PersonSet 上的鎖。PersonSet的狀態徹底由它的內置鎖保護,於是PersonSet是一個線程安全的類。this

1.Java監視器模式

  從線程封閉原則及其邏輯推論能夠得出Java監視器模式。遵循Java監視器模式的對象會把對象的全部可變狀態都封裝起來,並由對象本身的內置鎖來保護。例如在下面Counter 中封裝了一個私有狀態變量value,對該變量的全部訪問都須要經過Counter的方法來執行,而且這些方法都是同步的。 spa

@ThreadSafe
public final class Counter {
    @GuardedBy("this") private long value = 0;

    public synchronized long getValue() {
        return value;
    }

    public synchronized long increment() {
        if (value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        return ++value;
    }
}

  Java監視器模式僅僅是一種編寫代碼的約定,對於任何一種鎖對象,只要自始至終都使用該鎖對象,均可以用來保護對象的狀態。好比下面的示例:線程

public class PrivateLock {
    private final Object myLock = new Object();
    @GuardedBy("myLock") Widget widget;

    void someMethod() {
        synchronized (myLock) {
            // Access or modify the state of widget
        }
    }
}

2.示例:車輛追蹤

  例如出租車、警車、貨車等。首先使用監視器模式來構建車輛追蹤器,而後再嘗試放寬某些封裝性需求同時又保持線程安全性。 每臺車都由一個String對象來標識,而且擁有一個相應的位置座標(x,y)。在 VehicleTracker類中封裝了車輛的標識和位置,於是它很是適合做爲基於MVC (Model-View-Controller,模型–視圖–控制器)模式的GUI應用程序中的數據模型,而且該模型將由一個視圖線程和多個執行更新操做的線程共享。視圖線程會讀取車輛的名字和位置,並將它們顯示在界面上。設計

  相似地,執行更新操做的線程經過從GPS 設備上獲取的數據或者調度員從GUI界面上輸入的數據來修改車輛的位置。code

@ThreadSafe
 public class MonitorVehicleTracker {
    @GuardedBy("this") 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<String, MutablePoint>();

        for (String id : m.keySet())
            result.put(id, new MutablePoint(m.get(id)));

        return Collections.unmodifiableMap(result);
    }
}

@NotThreadSafe
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;
    }
}
 

  雖然類MutablePoint 不是線程安全的,但追蹤器類是線程安全的。它所包含的Map對象和可變的Point對象都不曾發佈。當須要返回車輛的位置時,經過 MutablePoint 拷貝構造函數或者deepCopy方法來複制正確的值,從而生成一個新的Map對象,而且該對象中的值與原有Map對象中的key值和value值都相同。對象

 

三.線程安全性的委託

  在前面的CountingFactorizer類中,咱們在一個無狀態的類中增長了一個AtomicLong類型的域,而且獲得的組合對象仍然是線程安全的。因爲CountingFactorizer的狀態就是AtomicLong的狀態,而AtomicLong是線程安全的,所以CountingFactorizer不會對counter的狀態施加額外的有效性約束,因此很容易知道CountingFactorizer是線程安全的。咱們能夠說CountingFactorizer將它的線程安全性委託給AtomicLong 來保證:之因此CountingFactorizer是線程安全的,是由於AtomicLong是線程安全的。

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

  下面將介紹一個更實際的委託示例,構造一個委託給線程安全類的車輛追蹤器。咱們將車輛的位置保存到一個Map對象中,所以首先要實現一個線程安全的Map類,ConcurrentHashMap。咱們還能夠用一個不可變的Point類來代替MutablePoint以保存位置,以下面程序所示。

@Immutable
public class Point {
    public final int x, y;

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

  因爲Point類是不可變的,於是它是線程安全的。不可變的值能夠被自由地共享與發佈,所以在返回location時不須要複製。 在下面程序 DelegatingVehicleTracker中沒有使用任何顯式的同步,全部對狀態的訪問都由ConcurrentHashMap來管理,並且Map全部的鍵和值都是不可變的。

@ThreadSafe
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);
    }

    // Alternate version of getLocations (Listing 4.8)
    public Map<String, Point> getLocationsAsStatic() {
        return Collections.unmodifiableMap(
                new HashMap<String, Point>(locations));
    }
}

3.當委託失效時

  下面的NumberRange使用了兩個 AtomicInteger來管理狀態,而且含有一個約束條件,即第一個數值要小於或等於第二個數值。

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不是線程安全的,沒有維持對下界和上界進行約束的不變性條件。setLower和setUpper等方法都嘗試維持不變性條件,但卻沒法作到。setLower和setUpper 都是「先檢查後執行」的操做,但它們沒有使用足夠的加鎖機制來保證這些操做的原子性。假設取值範圍爲(0,10),若是一個線程調用setLower(5),而另外一個線程調用setUpper(4),那麼在一些錯誤的執行時序中,這兩個調用都將經過檢查,而且都能設置成功。結果獲得的取值範圍就是(5,4),那麼這是一個無效的狀態。所以,雖然 AtomicInteger是線程安全的,但通過組合獲得的類卻不是。因爲狀態變量lower和upper 不是彼此獨立的,所以NumberRange不能將線程安全性委託給它的線程安全狀態變量。

4.發佈底層的狀態變量

  若是一個狀態變量是線程安全的,而且沒有任何不變性條件來約來它的值,在變量的操做上也不存在任何不充許的狀態轉換,那麼就能夠安全地發佈這個變量。

5.示例:發佈狀態的車輛追蹤器

  下面SafePoint提供的get方法同時得到x和y的值,並將兩者放在一個數組中返回。若是爲x和y分別提供get方法,那麼在得到這兩個不一樣座標的操做之間,x和y的值發生變化,從而致使調用者看到不一致的值﹔車輛歷來沒有到達過位置(x,y)。經過使用SafePoint,能夠構造一個發佈其底層可變狀態的車輛追蹤器,還能確保其線程安全性不被破壞,以下面的Publishing VehicleTracker類所示。 

@ThreadSafe
public class SafePoint {
    @GuardedBy("this") 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.set(x, y);
    }

    public synchronized int[] get() {
        return new int[]{x, y};
    }

    public synchronized void set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}


@ThreadSafe
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);
    }
}

 

四.在現有的線程安全類中添加功能

  好比須要一個這樣的需求:若是一個線程安全的list中沒有某個X元素,則添加。因爲這個操做不是原子的,因此並非線程安全的,可能兩個線程同時檢測到X不存在,而後同時將X添加進了List中。

1.客戶端加鎖機制

@ThreadSafe
class GoodListHelper <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;
        }
    }
}

2.組合

@ThreadSafe
public class ImprovedList<T> implements List<T> {
    private final List<T> list;

    /**
     * PRE: list argument is thread-safe.
     */
    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;
    }
}

  ImprovedList經過自身的內置鎖增長了一層額外的加鎖。它並不關心底層的List是不是線程安全的,即便List 不是線程安全的或者修改了它的加鎖實現,ImprovedList 也會提供一致的加鎖機制來實現線程安全性。雖然額外的同步層可能致使輕微的性能損失,但與模擬另外一個對象的加鎖策略相比,ImprovedList更爲健壯。事實上,咱們使用了Java監視器模式來封裝現有的List,而且只要在類中擁有指向底層List的惟一外部引用,就能確保線程安全性。

相關文章
相關標籤/搜索