4. 對象的組合

4.1 設計線程安全的類

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

  • 找出構成對象狀態的所有變量
  • 找出約束變量的不可變性
  • 建立對象狀態的并發訪問管理
public final class Counter{
    private long value = 0;

    public synchronized long getValue(){
        return value;
    }

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

如上,我們構造了一個線程安全的類,我們來找出它所滿足的三個條件:

  • 變量:value變量
  • 約束:改變value前,判斷value是否達到最大值```if (value == long.MAX_VALUE)
- 并發管理:通過synchronized關鍵字

##### 4.1.1 收集同步需求
> 如果不了解對象的不變性條件與后驗條件,那么就不能保證線程的安全性。要滿足在狀態變量的有效值或狀態轉換上的各種約束條件,就需要借助于原子性和封裝性。

我們先來理解這兩個名詞:
- 不可性條件:在許多類中都定義了一些不可變條件,用于判斷狀態是否有效。比如,在上面的例子中,value的值必須滿足在```Long.MIN_VALUE```和```Long.MAX_VALUE```之間。

- 后驗條件:比如上面counter中當前狀態為17,那么下一個有序狀態只能為18。當下一個狀態需要依賴上一個狀態時,這個操作就必須是一個復合操作。

##### 4.1.2 依賴狀態的操作
> 如果在某個操作中包含有基于狀態的先驗條件,那么這個操作就稱為“依賴狀態的操作”。比如,在不能從空隊列中刪除元素,因此,在刪除元素之前需要檢查隊列是否為空。

##### 4.1.3 狀態的所有權
>狀態變量的所有者將決定采用何種加鎖協議來維持狀態的完整性。所有權意味著控制權。但,如果發布了某個可變對象的引用,那么就不再擁有獨立的控制權,最多是“共享控制權”。

#### 4.2 實例封閉
> 將數據封裝在對象內部,可以將數據的訪問限制在對象的方法上,從而更容易確保在訪問數據時總能持有正確的鎖。

注意:被封閉的對象一定不能超過它們既定的作用域。對象可以封閉在類的一個實例(比如私有成員)中,或者封閉在某個作用域內(比如一個局部變量),再或者封閉在線程內。

public class PersonSet{
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);
}

}

如上,我們將PersonSet進行了實例封閉,將數據mySet設置為私有成員,并通過方法來對數據進行訪問。

##### 4.2.1 Java監視器模式
> 對于任何一種鎖對象,只要自始自終都使用該鎖對象,都可以用來保護對象的狀態。

public class privateLock{
private final Object myLock = new Object();
Widget widget;

void someMethod(){
    synchronized(myLock){
        //訪問或修改Widget的狀態
    }
}

}

如上為通過一個私有鎖來保護狀態

###### 示例:基于監視器模式的車輛追蹤
我們要求視圖線程和更新線程并發地訪問數據模型,因此該模型必須是線程安全的。

- 記錄軌跡的點:

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 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 void setLocations(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 Map<String,MutablePoint> deepCopy(Map<String, MutablePoint> map){
Map<String,MutablePoint> result = new HashMap<>();
for (String id : map.keySet())
result.put(id,new MutablePoint(map.get(id)));
return Collections.unmodifiableMap(result);
}
}

- 模擬更新和訪問數據:

public static void main(String[] args){
//初始化數據
Map<String,MutablePoint> map = new HashMap<>();
map.put("test1",new MutablePoint());
map.put("test2",new MutablePoint());
map.put("test3",new MutablePoint());

MonitorVehicleTracker tracker = new MonitorVehicleTracker(map);
//設置更新線程,每隔10s更新一次數據
Thread set_thread = new Thread(){
    @Override
    public void run() {
        while (true){
            Map<String,MutablePoint> locations = tracker.getLocations();
            for (String key : locations.keySet()){
                MutablePoint p = locations.get(key);
                int dx = (int) (Math.random() * 10);
                int dy = (int) (Math.random() * 10);
                tracker.setLocations(key, p.x+dx, p.y+dy);
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
//設置視圖線程,每隔10s獲取一次數據,并顯示
Thread get_thread = new Thread(){
    @Override
    public void run() {
        while(true){
            Map<String,MutablePoint> locations = tracker.getLocations();
            for (String key : locations.keySet()){
                MutablePoint p = locations.get(key);
                System.out.println("key: " + key + " value: (" + p.x
                    + ","+p.y + ")");
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
};

set_thread.start();
get_thread.start();

}


- 總結:對于上面的模式,類MutablePoint不是線程安全的,但追蹤器類是線程安全的。因為它所包含的map對象和可變的Point對象都未曾發布過。當需要返回車輛的位置時,通過MutablePoint拷貝構造函數來復制正確的值,從而生成一個新的對象。

- 評價:優點是location集合上內部的數據是一致的,缺點是每次調用getLocation都需要復制數據,這將影響到性能。

#### 4.3 線程安全性的委托

public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);

public long getCount() {return count.get();}

public void service(ServletRequest req, ServletResponse resp){
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = factors(i);
    count.incrementAndGet();
    encodeIntoResponse(resp,factors);
}

}

>  如上,我們將CountingFactorizer類的線程安全性委托給AtomicLong來保證:之所以CountingFactorizer是安全的,是因為AtomicLong是安全的。

###### 4.3.1 示例:基于委托的車輛追蹤器
- 我們用不可變的Point類代替MutablePoint類:

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

這樣能保證在返回location時,不需要復制。因為不可變的值是可以被自由地共享。

- 將線程安全委托給ConcurrentHashMap

public class DelegatingVehicleTracker{
private final ConcurrentHashMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;

public DelegatingVehicleTracker(Map<String,Point> map){
    locations = new ConcurrentHashMap<String,Point>(map);
    unmodifiableMap = Collections.unmodifiableMap(map);
}

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

}

我們可以看到將線程安全性交給了安全容器ConcurrentHashMap,從而免去了對方法的加鎖。

 ##### 4.3.2 獨立的狀態變量
> 我們可以將線程安全性委托給多個狀態變量,只要這些變量是彼此獨立的,即組合而成的類并不會在其包含的多個狀態變量上增加任何不變性條件。

public class VisualComponent{
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();

public void addKeyListener(KeyListener listener){
    keyListeners.add(listener);
}

public void addMouseListener(MouseListener listener){
    mouseListeners.add(listener);
}

public void removeKeyListener(KeyListener listener){
    keyListeners.remove(listener);
}

public void removeMouseListener(MouseListener listener){
    mouseListeners.remove(listener);
}

}

如上,我們將鍵盤和鼠標監聽器列表都委托給CopyOnWriteArrayList,因為兩者之間是相互獨立的,因此不會增加不變性條件。

##### 4.3.3 當委托失效時
> 如果一個類是由多個獨立且線程安全的狀態變量組成,并且在所有的操作中都不包含無效狀態,那么可以將線程安全性委托給底層的狀態變量。

public class NumberRange{
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 lower 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");
    lower.set(i);
}

public boolean isInRange(int i){
    return (i >= lower.get() && i <= upper.get());
}

}

如上,雖然AtomicInteger是線程安全的,但經過組合得到的類卻不安全。由于狀態變量lower和upper并不是彼此獨立的,因此NumberRange不能將線程安全性委托給它的安全狀態變量。

如果某個類含有復合操作,僅靠委托并不足以實現線程安全性時,需要提供自己的加鎖機制以保證這些復合操作都是原子性。

##### 4.3.4 發布底部的狀態變量
> 如果一個這個變量是線程安全的,并且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態轉換,那么就可以安全地發布這個變量。

##### 4.3.5 發布狀態的車輛追蹤器
- 定義一個安全且可變的Point類:

public class SafePoint{
private int x,y;
private SafePoint(int[] a) {this(a[0],a[1]);}

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

}

注意:我們這里將x和y綁定在一起,因為如果分別為x和y提供get方法,那么在獲得這兩個不同坐標的操作之間,x和y的值發生變化,從而導致調用者看到不一致的值。

- 安全發布底層狀態的車輛追蹤器:

public class PublishingVehicleTracker{
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;

public PublishingVehicleTracker(Map<String,SafePoint> map){
    locations = new ConcurrentHashMap<String,SafePoint>(map);
    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(
            "Invalid vehicle name: " + id);
    locations.get(id).set(x,y);
}

}

注意:PublishingVehicleTracker將其線程安全性委托給底層的ConcurrentHashMap,只是Map的元素是線程安全且可變的Point。getLocation方法返回底層Map對象的一個不可變副本。調用者不能增加或刪除車輛,但卻可以通過修改返回Map中的SafePoint值來改變車輛的位置。

#### 4.4 在現有的線程安全類中添加功能
> 假設需要一個線程安全的鏈表,它需要提供一個原子的“若沒有則添加”的操作。

- 對原有的類進行擴展:

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

評價:如果底層的類改變了同步策略并選擇了不同的鎖來保護它的狀態量,那么子類會被破壞。因為在同步策略改變后它無法再使用正確的鎖來控制對基類狀態的并發訪問。

- 擴展類的功能:

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

}

評價:該中方式并不能保證線程的安全性,因為List所用的鎖和ListHelper所用的鎖不是同一個鎖。這意味著putIfAbsent相對于list的其他操作來說不是原子的。


- 客戶端加鎖:對于使用某個對象X的客戶端代碼,使用X本身用于保護其狀態的鎖來保護這段客戶代碼。

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

}

如此,就能保證ListHelper所用的鎖和list所用的鎖是一致的。

##### 4.4.2 組合:實現接口的方式

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 absent = !list.contains(x);
    if (absent)
        list.add(x);
    return absent;
}

}

ImprovedList通過自身的內置鎖增加了一層額外的加鎖來實現線程的安全性。













最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,778評論 18 399
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,373評論 11 349
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,143評論 0 62
  • 最近回頭一看,發現我們的項目現在對圖片處理都是用YYWebImage 的處理方式方式的,用了不短時間了,卻沒有好好...
    炸街程序猿閱讀 1,286評論 0 1