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