Java 併發程式設計實踐 讀書筆記四

腹黑客發表於2020-12-13

組合物件

設計執行緒安全的類

設計執行緒安全類的過程應該包括下面3個基本要素:

  • 確定物件狀態是由哪些變數構成的
  • 確定限制狀態變數的不變約束
  • 制定一個管理併發訪問物件狀態的策略

物件狀態從域講,如果物件域都是基本型別(primitive)的,那麼這些域組成了物件的完整狀態。

同步策略定義了物件如何協調對其他狀態的訪問,並且不會違反它的不變約束後或後驗條件。

Java監視器模式的簡單執行緒安全計數器
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;
		}
	}
}
  • 收集同步需求

    物件與變數擁有一個狀態空間:即它們可能儲於的狀態範圍。

    很多類通過不可變約束來判斷某一種狀態是合法的還是非法的。

    操作的後驗條件會指出某種狀態轉換是非法的。

    不變約束與後驗條件施加在狀態及狀態轉換上的約束,引入了額外的同步與封裝的需要。

    一個類的不變約束也可以約束多個狀態變數。

       不理解物件的不可變約束和後驗條件,就不能保證執行緒安全性,要約束狀態變數的有效值或者狀態轉換,就需要原子性與封裝性。
    
  • 狀態依賴的操作

    類的不變約束與方法的後驗條件約束了物件合法的狀態和合法狀態轉換。

    某些物件的方法也有基於狀態的先驗條件。

    例如:
    無法從空佇列中移除一個條目:在你刪除元素前,佇列必須儲於“非空”狀態。若一個操作存在基於狀態的先驗條件,則把它稱為是狀態依賴的。
    
  • 狀態所有權

    很多情況下,所有權與封裝性總是一起出現的:物件封裝它擁有的狀態,且擁有它封裝的狀態。擁有給定狀態的所有者決定了。

    類通常不會擁有由建構函式或方法傳遞進來的物件,除非該方法是被明確設計用來轉換傳遞物件的所有權的。

    容器類通常表現出一種“所有權分離”的形式。

    為了防止多執行緒併發訪問同一物件時所帶來的干擾,這些物件應該是執行緒安全物件,高效不可變物件或者由鎖明確保護的物件。

例項限制

即使一個物件不是執行緒安全的,任然由許多技術可以讓它安全地用於多執行緒程式。

通過使用例項限制,封裝簡化了類的執行緒安全化工作,通常稱為限制。把限制與各種適當的鎖策略相結合,可以確保程式以執行緒安全的方式使用其他非執行緒安全物件。

將資料封裝在物件內部,把對資料的訪問限制在物件的方法上,更易確保執行緒在訪問資料時總能獲得正確的值。

被限制物件一定不能逃逸到它的期望可用範圍之外。

使用限制確保執行緒的安全。
@ThreadSafe
public class PersonSet{
	@GuardedBy("this")
	private final Set<Person> mySet = new HashSet<>();
	
	public synchronized void addPerson(Person p){
		mySet.add(p);
	}
	
	public synchronzied boolean containsPerson(Person p){
		return mySet.contains(p);
	}
}

例項限制是構造執行緒安全類的最簡單的方法之一。

限制性使構造執行緒安全的類變得更容易,因為類的狀態被限制後,分析它的執行緒安全性時,就不必檢查完整的程式。
  1. Java監視器模式

    執行緒限制原則的直接推論之一是Java監視器模式。遵循Java監視器模式的物件封裝了所有的可變狀態,並由物件自己的內部鎖保護。

    // 私有鎖保護狀態
    public class PrivateLock{
    	private final Object myLock = new Object();
    	@GuardedBy("myLock") Widget widget;
    	
    	void someMethod(){
    		synchronized(myLock){
    			// 訪問或修改widget的狀態
    		}
    	}
    }
    

    使用私有鎖物件,而不是物件的內部鎖(或任何其他可以公共訪問的鎖),有很多好處。

    • 私有鎖物件可以封裝幀。
  2. 使用監視器模式構造一個具有實際意義的案例

    // 基於監視器的機動車追蹤器實現
    @ThreadSafe
    public class MonitorVehicleTracker{
    	@GuardedBy("this")
    	private final Map<String,MutablePoint> locations;
    	
    }
    

幾乎所有的物件都是組合物件。當憑空構造一個類,或者使用非執行緒安全物件組裝一個i類時,Java監視器模式釋放有用。

委託執行緒安全

在CountingPactorizer中,我們向一個無狀態的類加入了一共AtomicLong型別的屬性,所得組合物件仍然是執行緒安全的。因為CountingPactorizer的狀態就是執行緒安全的,而且CountingPactorizer並未對counter的狀態施加額外的有效性約束,所有,很顯然CountingPactorizer是執行緒安全的。我們可以說CountingPactorizer將它的執行緒安全性委託了給AtomicLong,因為AtomicLong是執行緒安全地,所有CountingPactorizer也是。

  1. 使用委託的機動車追蹤器

    不可變的Point類取代MutablePoint,來儲存location資訊
    @Immutable
    public class Point{
    	public final int x,y;
    	
    	public Point(int x,int y){
    		this.x=x;
    		this.y=y;
    	}	
    }
    Point類是不可變的,因而是執行緒安全的。
    
    // 將執行緒安全委託到ConcurrentHashMap
    public class DelegatingVehicleTracker{
    	private final ConcurrentHashMap<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 void setLocation<String id,int x,int y){
    		if(locations.replace(id,new Point(x,y)) == null){
    			throw new IllegalArgumentException("invalid vehicle name:"+id);
    		}
    	}
    }
    

    基於"監視器"的程式碼返回location的快照,基於"委託"的程式碼返回一共不可變,但是"現場"的location檢視。

  2. 非狀態依賴變數

    // 委託執行緒安全到多個底層的狀態變數
    
  3. 當委託無法勝任時

    如果一個類由多個彼此獨立的執行緒安全的狀態變數組成,並且類的操作不包含任何無效狀態轉換時,可以將執行緒安全委託給這些狀態變數。
    
  4. 釋出底層的狀態變數

    如果一個狀態變數是執行緒安全的,沒有任何不變約束限制它的值,並且沒有任何狀態轉換限制它的操作,那麼它可以被安全釋出。
    

    例子:

    // 可變的執行緒安全Point類
    @ThreadSafe
    public class SafePoint{
    	@GuardedBy("this") private int x,y;
    	
    	private SafePoint(int[] a){
    		this(a[0],a[1]);
    	}
    	
    	publicSafePoint(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;
    	}
    }
    // 安全釋出底層狀態的機動車追蹤器
    @ThreadSafe
    public class PublishingVehicleTracker{
    	private final Map<String,SafePoint> locations;
    	private final Map<String,SafePoint> unmodifiableMap;
    	
    	public PublishingVehicleTracker(Map<String,Point> 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);
    	}
    }
    

向已有的執行緒安全類新增功能

構建"缺少即加入"操作。

“缺少即加入”的概念相當直觀 – 在向容器中新增一個元素先檢視釋放已存在,若存在則不新增。這種操作必須是原子的。

新增一個新原子操作的最安全方法是,修改原始的類,以支援期望的操作。但通常是不可能的。

另一個方法是擴充套件這個類,假如這個類在設計啥上是可以擴充套件的。

  1. 客戶端鎖

    // 非執行緒安全的 "缺少即加入" 
    @NotThreadSafe
    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上,ListHelper僅僅描繪了同步的幻象,即使一個list的操作全部申明為synchronized,但使用了不同鎖將意味著putIfAbsent對於List的其他操作而言,不是原子化的。

    為了能正確,我們要保證List用於客戶端加鎖與外部加鎖時所使用的鎖是同一個鎖。

    @ThreadSafe
    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;
    		}
    	}
    }
    
  2. 組合

    向已有的類中新增一個原子操作,還有更健壯的選擇:組合。

    // 使用組合(composition) 實現 “缺少即加入”
    @ThreadSafe
    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();}
    	// .. similarly delegate other List methods
    }
    

同步策略的文件化

在維護執行緒安全的過程中,文件是最強大的工具之一。

為類的使用者編寫類執行緒安全性擔保的文件;為類的維護者編寫類的同步策略檔案。

每次使用synchronized,volatile或者任何執行緒的安全類,都表現了一種同步策略,這項策略是為了確保處於併發訪問中的資料的完整性。

同步策略起碼需編寫內容

1>. 哪些變數申明為volatile型別
2>. 哪些變數被鎖保護
3>. 哪個鎖保護哪些變數
4>. 哪些變數是不可變的或者被限制線上程中的。
5>. 哪些操作必須是原子的

類的執行緒安全性擔保文件

1>. 執行緒是安全的
2>. 它的回撥釋放持有一個鎖
3>. 有沒有特定的鎖會影響它的行為
如果你利用鎖保護狀態,也要編寫文件,以便日後維護。
  1. 含糊不清的文件

    很多Java技術規範對於介面的執行緒安全性的擔保和條件都隻字未提,或者只有直言片語,比如ServletContext,HttpSession或DataSource。

    只能猜測。有一種方式可以提高你猜測的準確性:從規範實現者的角度去解讀規範,而不是從規範使用者的角度去看。

相關文章