JUC系列三:對象的委託與組合

在講以前,咱們先看一個Java監視器模式示例,這個示例是用於調度車輛的車輛追蹤器,首先使用監視器模式來構建車輛追蹤器,而後再嘗試放寬某些封裝性需求同時又保持線程安全性。每臺車都由一個String對象來標識,而且擁有一個相應的位置座標。在MonitorVehicleTracker類中封裝了全部車輛的標識和位置,且這個類將被一個讀取線程和多個更新線程所共享,因此這個類必定要是線程安全的。java

import java.util.*;
/**
	基於監視器模式的線程安全的車輛追蹤
	MutablePoint沒有被髮布出去,發佈出去的只是一個數據與MutablePoint相同的新對象,因此修改MutablePoint的值只能經過setLocation來實現
*/
public class MonitorVehicleTracker{
	private final Map<String,MutablePoint> locations;
	
	/**
		構造時採用的是深拷貝,若是不採用深拷貝,那麼在構造完後,locations就被髮布出去了,這樣裏面的數據就可能被不安全的修改
		好比
		
		Map<String,MutablePoint> locations=...
		MonitorVehicleTracker m=new MonitorVehicleTracker(locations);
		...

		構造完後就能夠在這之後的代碼往locations中添加和刪除車輛,這是不安全的
	*/
	public MonitorVehicleTracker(Map<String,MutablePoint> locations){
		this.locations=deepCopy(locations);
	}
	public synchronized Map<String,MutablePoint> getLocations(){
		return deepCopy(locations);
	}
	/**
		防止MutablePoint被髮布出去,返回一個數據相同的新的對象,
	*/
	public synchronized MutablePoint getLocation(String id){
		MutablePoint point=locations.get(id);
		return point==null?null:new MutablePoint(point);
	}
	public synchronized void setLocation(String id,int x,int y){
		MutablePoint point=locations.get(id);
		if(point==null){
			throw new IllegalArgumentException("No Such Id: "+id);
		}
		point.x=x;
		point.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)));//防止locations中的MutablePoint被髮布出去,返回一個數據相同的新對象
		}
		return Collections.unmodifiableMap(result);//返回一個不能被修改的map
	}
}
/**
	非線程安全
*/
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不是線程安全的,但因爲MonitorVehicleTracker是線程安全的,其共有方法都使用了內置鎖進行同步,而且它所包含的Map對象和可變的MutablePoint都不曾發佈。當須要返回車輛的位置時,經過MutablePoint拷貝函數或者deepCopy方法複製正確的值,從而生成一個鍵值對都與原Map對象都相同的新的Map對象。 但這也會產生一個性能上的問題,當車輛較多時,複製數據會佔用較多的時間。並且,當複製完成後,若是車輛的位置發生了變化,並不會即便反應到讀取線程中去,這是好是壞就依狀況而定了。爲了解決這些問題,接下來咱們就使用其它的方式來實現這個線程安全類安全

###委託函數

所謂委託,就是使用一個本來就線程安全的類來管理類的某個或某些狀態,在上面的例子中,線程安全主要體如今locations狀態上,因此,咱們如今將locations委託給線程安全的ConcurrentHashMap。性能

import java.util.*;
import java.util.concurrent.*;
/**
	不使用deepCopy
	基於委託的車輛追蹤器
	雖然Point被髮布出去了,可是unmodifiableMap添加或刪除替換裏面的車輛,而Point又是不可變的,因此跟沒發佈出去同樣
	只能經過MonitorVehicleTracker1的setLocation方法來修改Point
*/
public class MonitorVehicleTracker1{
	private final ConcurrentMap<String,Point> locations;
	private final Map<String,Point> unmodifiableMap;

	public MonitorVehicleTracker1(Map<String,Point> map){
		locations=new ConcurrentHashMap<String,Point>(map);
		unmodifiableMap=Collections.unmodifiableMap(locations);
	}
	/**
		MonitorVehicleTracker類中返回的是快照,而在這裏返回的是不可修改但卻實時的車輛位置視圖,這就意味着若是返回後車輛的位置發生了變化,對於視圖線程來講,是可見的。
	*/
	public Map<String,Point> getLocations(){
		return unmodifiableMap;
		//return Collections.unmodifiableMap(new HashMap<String,Point>(locations));
	}
	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);
		}
	}
}
/**
	一個不可變的Point類
	Point必須是不可變的,由於getLocations會發佈一個含有可變狀態的Point引用
*/
class Point{
	....
}

咱們能夠看到,當使用線程安全的類去管理locations時,對於locations的get和replace都將是線程安全的,因此不須要像第一個例子同樣,對getLocation和setLocation進行同步,而對於getLocations方法來講,返回的是一個不可修改的Map,因此也不用擔憂Map中的對象被更新或者是刪除。可是,儘管如此,對於Map中的Point對象,咱們仍是能夠修改的裏面的屬性值的(Point對象被髮布出去了),並且getLocation方法也發佈了一個Point對象,因此,這也是不安全的,所以,咱們要麼將Point對象設置成不可變的,要麼不發佈Point,在這裏,咱們選擇前者。this

/**
	一個不可變的Point類
	Point必須是不可變的,由於getLocations會發佈一個含有可變狀態的Point引用
*/
class Point{
	public final int x,y;
	public Point(int x,int y){
		this.x=x;
		this.y=y;
	}
}

在調用getLocations時,返回給讀線程的是一個Collections.unmodifiableMap(locations)對象,因此若是寫線程使用了setLocation來更新車輛的信息,讀線程都能及時的看到。線程

對於Point來講,它要麼是不可變的,要麼不會被髮布出去,可是,當咱們須要發佈一個可變的Point時,咱們就須要建立一個線程安全的可變Point,這樣,即便是被髮布出去,也保證了線程安全性。code

public 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;
	}
}
public class MonitorVehicleTracker2{
	private final Map<String,SafePoint> locations;
	private final Map<String,SafePoint> unmodifiableMap;
	public MonitorVehicleTracker2(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);//set是線程安全的
	}
}

固然,上面只是個很簡單的例子,對於複雜的共享類來講,多個狀態之間可能會存在不變性,這時,若是隻是使用線程安全類來委託的話,並不能解決線程安全問題。像下面的例子對象

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");
		}
		upper.set(i);
	}

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

在上面的例子中,存在lower<upper的不變性約束,儘管setXXX方法使用了先檢查後執行對這個不變性進行了維持,但並無使用足夠的加鎖機制來保證這些操做的原子性,試想若是兩個方法同時被兩個線程分別調用,那麼就可能破壞這種不變性。因此僅靠委託是不夠的,還必須對這些操做進行加鎖同步。get

###組合 當咱們要爲一個線程安全的類添加一個新的操做時,咱們能夠選擇多種方式,像修改原始的類,雖然這彷佛並不怎麼理想(由於你不必定能獲取到原始類的源碼),也能夠擴展這個類。好比咱們要給Vector添加一個若沒有則添加的操做。同步

public class MyVector<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 boolean putIfAbsent(E x){
		synchronized(list){
			boolean absent=!list.contains(x);
			if(absent){
				list.add(x);
			}
			return absent;
		}
	}
}

固然,使用這種方式咱們必須使list操做加鎖時和putIfAbsent操做加鎖時使用的是同一個鎖,不然就是非線程安全的。

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

很明顯,putIfAbsent操做的加鎖對象是ListHelper對象,而list的操做的加鎖對象是list,加鎖對象不一樣,就不能保證線程的安全性。

當爲現有的類添加一個操做時,爲了保證原有類的線程安全,咱們還可使用更好的方式,那就是組合。咱們能夠將List對象的操做委託給底層的List實例來實現List的操做,同時還添加一個原子的putIfAbsent方法,這樣,List實例就不能直接被訪問,而是要經過封裝類來實現。

public class MyList<E> implements List<T>{
	private final List<T> list;
	public MyList<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 add(T x){...}
	// ... 使用內置鎖實現 list的其它方法
}

因此,無論list是否是線程安全的,MyList也都提供了一致的加鎖機制來實現線程安全性。但這樣的實現,會產生一些性能上的損失,但實現更加健壯。

相關文章
相關標籤/搜索