JUC系列三:对象的委托与组合

原创
2015/08/05 23:53
阅读数 358

在讲之前,我们先看一个Java监视器模式示例,这个示例是用于调度车辆的车辆追踪器,首先使用监视器模式来构建车辆追踪器,然后再尝试放宽某些封装性需求同时又保持线程安全性。每台车都由一个String对象来标识,并且拥有一个相应的位置坐标。在MonitorVehicleTracker类中封装了所有车辆的标识和位置,且这个类将被一个读取线程和多个更新线程所共享,所以这个类一定要是线程安全的。

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,在这里,我们选择前者。

/**
	一个不可变的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,这样,即使是被发布出去,也保证了线程安全性。

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方法使用了先检查后执行对这个不变性进行了维持,但并没有使用足够的加锁机制来保证这些操作的原子性,试想如果两个方法同时被两个线程分别调用,那么就可能破坏这种不变性。所以仅靠委托是不够的,还必须对这些操作进行加锁同步。

###组合 当我们要为一个线程安全的类添加一个新的操作时,我们可以选择多种方式,像修改原始的类,虽然这似乎并不怎么理想(因为你不一定能获取到原始类的源码),也可以扩展这个类。比如我们要给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也都提供了一致的加锁机制来实现线程安全性。但这样的实现,会产生一些性能上的损失,但实现更加健壮。

展开阅读全文
打赏
0
3 收藏
分享
加载中
更多评论
打赏
0 评论
3 收藏
0
分享
返回顶部
顶部