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也都提供了一致的加锁机制来实现线程安全性。但这样的实现,会产生一些性能上的损失,但实现更加健壮。

相关文章
相关标签/搜索