4. 對象的組合

4.1 設(shè)計(jì)線程安全的類

在設(shè)計(jì)線程安全的類的過程中,需要包含以下的三個(gè)基本的要素:

  • 找出構(gòu)成對象狀態(tài)的所有變量
  • 找出約束變量的不可變性
  • 建立對象狀態(tài)的并發(fā)訪問管理
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;
    }
}

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

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

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

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

- 后驗(yàn)條件:比如上面counter中當(dāng)前狀態(tài)為17,那么下一個(gè)有序狀態(tài)只能為18。當(dāng)下一個(gè)狀態(tài)需要依賴上一個(gè)狀態(tài)時(shí),這個(gè)操作就必須是一個(gè)復(fù)合操作。

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

##### 4.1.3 狀態(tài)的所有權(quán)
>狀態(tài)變量的所有者將決定采用何種加鎖協(xié)議來維持狀態(tài)的完整性。所有權(quán)意味著控制權(quán)。但,如果發(fā)布了某個(gè)可變對象的引用,那么就不再擁有獨(dú)立的控制權(quán),最多是“共享控制權(quán)”。

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

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

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進(jìn)行了實(shí)例封閉,將數(shù)據(jù)mySet設(shè)置為私有成員,并通過方法來對數(shù)據(jù)進(jìn)行訪問。

##### 4.2.1 Java監(jiān)視器模式
> 對于任何一種鎖對象,只要自始自終都使用該鎖對象,都可以用來保護(hù)對象的狀態(tài)。

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

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

}

如上為通過一個(gè)私有鎖來保護(hù)狀態(tài)

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

- 記錄軌跡的點(diǎn):

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

}

- 實(shí)現(xiàn)車輛追蹤:

public class MonitorVehicleTracker {
private final Map<String, MutablePoint> locations;
//構(gòu)造函數(shù)
public MonitorVehicleTracker(Map<String,MutablePoint> locations){
this.locations = locations;
}
//返回?cái)?shù)據(jù)的拷貝對象
public synchronized Map<String,MutablePoint> getLocations(){
return deepCopy(locations);
}
//對數(shù)據(jù)進(jìn)行更新
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;
}
//實(shí)現(xiàn)對象數(shù)據(jù)的拷貝
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);
}
}

- 模擬更新和訪問數(shù)據(jù):

public static void main(String[] args){
//初始化數(shù)據(jù)
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);
//設(shè)置更新線程,每隔10s更新一次數(shù)據(jù)
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();
            }
        }
    }
};
//設(shè)置視圖線程,每隔10s獲取一次數(shù)據(jù),并顯示
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();

}


- 總結(jié):對于上面的模式,類MutablePoint不是線程安全的,但追蹤器類是線程安全的。因?yàn)樗膍ap對象和可變的Point對象都未曾發(fā)布過。當(dāng)需要返回車輛的位置時(shí),通過MutablePoint拷貝構(gòu)造函數(shù)來復(fù)制正確的值,從而生成一個(gè)新的對象。

- 評價(jià):優(yōu)點(diǎn)是location集合上內(nèi)部的數(shù)據(jù)是一致的,缺點(diǎn)是每次調(diào)用getLocation都需要復(fù)制數(shù)據(jù),這將影響到性能。

#### 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是安全的,是因?yàn)锳tomicLong是安全的。

###### 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時(shí),不需要復(fù)制。因?yàn)椴豢勺兊闹凳强梢员蛔杂傻毓蚕怼?
- 將線程安全委托給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 獨(dú)立的狀態(tài)變量
> 我們可以將線程安全性委托給多個(gè)狀態(tài)變量,只要這些變量是彼此獨(dú)立的,即組合而成的類并不會在其包含的多個(gè)狀態(tài)變量上增加任何不變性條件。

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

}

如上,我們將鍵盤和鼠標(biāo)監(jiān)聽器列表都委托給CopyOnWriteArrayList,因?yàn)閮烧咧g是相互獨(dú)立的,因此不會增加不變性條件。

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

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是線程安全的,但經(jīng)過組合得到的類卻不安全。由于狀態(tài)變量lower和upper并不是彼此獨(dú)立的,因此NumberRange不能將線程安全性委托給它的安全狀態(tài)變量。

如果某個(gè)類含有復(fù)合操作,僅靠委托并不足以實(shí)現(xiàn)線程安全性時(shí),需要提供自己的加鎖機(jī)制以保證這些復(fù)合操作都是原子性。

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

##### 4.3.5 發(fā)布狀態(tài)的車輛追蹤器
- 定義一個(gè)安全且可變的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綁定在一起,因?yàn)槿绻謩e為x和y提供get方法,那么在獲得這兩個(gè)不同坐標(biāo)的操作之間,x和y的值發(fā)生變化,從而導(dǎo)致調(diào)用者看到不一致的值。

- 安全發(fā)布底層狀態(tài)的車輛追蹤器:

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對象的一個(gè)不可變副本。調(diào)用者不能增加或刪除車輛,但卻可以通過修改返回Map中的SafePoint值來改變車輛的位置。

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

- 對原有的類進(jìn)行擴(kuò)展:

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

評價(jià):如果底層的類改變了同步策略并選擇了不同的鎖來保護(hù)它的狀態(tài)量,那么子類會被破壞。因?yàn)樵谕讲呗愿淖兒笏鼰o法再使用正確的鎖來控制對基類狀態(tài)的并發(fā)訪問。

- 擴(kuò)展類的功能:

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

}

評價(jià):該中方式并不能保證線程的安全性,因?yàn)長ist所用的鎖和ListHelper所用的鎖不是同一個(gè)鎖。這意味著putIfAbsent相對于list的其他操作來說不是原子的。


- 客戶端加鎖:對于使用某個(gè)對象X的客戶端代碼,使用X本身用于保護(hù)其狀態(tài)的鎖來保護(hù)這段客戶代碼。

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 組合:實(shí)現(xiàn)接口的方式

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通過自身的內(nèi)置鎖增加了一層額外的加鎖來實(shí)現(xiàn)線程的安全性。













最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容