实例封闭:将数据封装在对象中,将数据的访问限制在对象的方法上,确保线程在访问数据时总能持有正确的锁
java平台的类库中有很多线程封闭的示例,其中一些类的唯一用途就是将非线程安全的类转为线程安全的类。一些基本的容器类并非线程安全,如ArrayList和HashMap,但类库提供了包装器工厂方法,如Collections.synchronizedList,使这些非线程安全的类可以在多线程环境中安全地使用。
这些工厂方法通过“装饰器”模式将容器类封装在一个同步的封装器对象中,包装器将接口中的每个方法都实现为同步方法,并将调用请求转发到底层的容器对象上。只要包装器对象拥有对底层容器对象的唯一引用,那么它就是线程安全的。
监视器模式
遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护,其主要优势在于它的简单性。
监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。
private final Object lock = newObject();public voiddoSomething(){synchronized(lock){//...
}
}
私有的锁可以将锁封装起来,使客户代码无法获得锁,但客户代码可以通过公有方法访问锁,以便参与到它的同步策略
示例:调度出租车的车辆追踪器
/*** 每一台车都由一个相应的String对象来标识,并拥有一个相应的位置坐标(x,y)虽然Point并不是线程安全的,但追踪器类是线程安全的。
* 它所包含的可变的Point对象和Map对象都未曾发布。这种实现方式是通过返回客户代码之前复制可变的数据来维持线程的安全性,
* 这在车辆容器非常大的情况下将极大的降低性能。此外,由于每次调用getLocation就要复制数据,将出现一种错误情况。
* 虽然车辆的实际位置发生了变化,但返回的信息却保持不变。(线程T1获取位置是通过复制原对象,在线程T2更新是通过替换原对象,T2的替换操作对于T1是未知的)*/
public classDemo{private final Maplocations;public Demo(Maplocations){this.locations =deepCopy(locations);
}public synchronized MapgetLocations(){returndeepCopy(locations);
}public synchronizedPoint getLocation(String key){
Point p=locations.get(key);return p == null ? null : newPoint(p);
}public synchronized void setLocation(String key,int x,inty){
Point p=locations.get(key);if(p == null){throw new IllegalArgumentException("no such id" +key);
}
p.x=x;
p.y=y;
}private static Map deepCopy(Maplocations){
Map copy = new HashMap();for(Entryentry : locations.entrySet()){
copy.put(entry.getKey(), entry.getValue());
}returncopy;
}
}classPoint{public intx;public inty;publicPoint(){
x= 0;
y= 0;
}publicPoint(Point p){this.x =p.x;this.y =p.y;
}
}
改进:线程安全性委托
/*** 将车辆的位置保存到一个Map对象中,首先实现一个线程安全的Map类,ConcurrentHashMap。
* 在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改但却实时的车辆位置。
* 线程T1调用getLocations,线程T2随后更新了某些点的位置。那么在返回给线程T1的Map中将反映出来,这样能获取更新的数据。
* 但同时也可能导致不一致的车辆位置视图,具体视需求而定。另外,Point类是不可变的,因而它是线程安全的。
* 不可变的值可以被自由地共享与发布,因此返回location时不需要复制。如果是可变对象,getLocation会发布一个指向可变对象的非线程安全的引用。*/
public classDemo{private final ConcurrentMaplocations;private final MapunmodifiableMap;public Demo(Mappoints){
locations= new ConcurrentHashMap(points);
unmodifiableMap=Collections.unmodifiableMap(locations);
}public MapgetLocations(){returnunmodifiableMap;
}publicPoint getLocation(String key){returnlocations.get(key);
}public void setLocation(String key,int x,inty){if(locations.replace(key, new Point(x,y)) == null){throw new IllegalArgumentException("no such id" +key);
}
}
}classPoint{public final intx;public final inty;public Point(int x,inty){this.x =x;this.y =y;
}
}
示例中将线程安全委托给单个线程安全的变量(集合),其实还可以将线程安全性委托给多个状态变量,只要这些变量是相互独立的,即组合而成的类不会在其包含的多个状态变量上增加任何不可变条件。因此如果底层变量是线程安全且彼此独立,则可以发布底层的状态变量,即将线程安全性委托给底层状态变量。
改进:发布底层的状态变量
/*** 调用者不能增加或删除车辆,但可以通过修改返回Map中的Point值来改变车辆的位置。
* Point的同步方法get()可以同时获取x和y的值,如果为x和y分别提供get方法,那么在获得这两个不同坐标的操作之间,
* x和y的值可能发生变化,从而导致调用者看到不一致的值。
* 另外在拷贝方法Point(Point p)中实现为this(p.get()),而不是this(p.x,p.y),可以避免这种竞态条件。*/
public classDemo{private final ConcurrentMaplocations;private final MapunmodifiableMap;public Demo(Mappoints){
locations= new ConcurrentHashMap(points);
unmodifiableMap=Collections.unmodifiableMap(locations);
}public MapgetLocations(){returnunmodifiableMap;
}publicPoint getLocation(String key){returnlocations.get(key);
}public void setLocation(String key,int x,inty){if(!locations.containsKey(key)){throw new IllegalArgumentException("no such id" +key);
}
locations.get(key).set(x, y);
}
}classPoint{private intx;private inty;private Point(int[] a){this(a[0],a[1]);
}publicPoint(Point p){this(p.get());
}public Point(int x,inty){this.x =x;this.y =y;
}public synchronized int[] get(){return new int[]{x,y};
}public synchronized void set(int x,inty){this.x =x;this.y =y;
}
}
提问:若要添加一个新的原子操作 “若没有则添加”
1. 最安全的方法是修改原始的类,但这通常无法做到,因为无法访问或修改源代码。而且要想修改原始的类,需要理解代码中的同步策略,这样增加的功能才能与原有的设计保持一致。
2. 另一种方法是扩展这个类,假设这个类在设计时考虑了可扩展性。
/*** 扩展Vector很简单,但并非所有的类都像Vector那样将状态向子类公开,因此也就不适合采用这种方法。
* 并且扩展比直接将代码添加到类中更脆弱,如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,
* 因为同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问(Vector在规范中定义了它的同步策略,因此不存在问题)。
*@param*/
public class ExtendVector extends Vector{public synchronized booleanputIfAbsent(E x){boolean absent = !contains(x);if(absent){
add(x);
}returnabsent;
}
}
3. 第三种策略是扩展类的功能,但并不是扩展类本身,而是将代码放入一个‘辅助类’中,在客户端加锁。
错误的加锁方式:
//list并不是使用的Demo上的锁来保护它的状态,Demo只是带来了同步的假象,并不能保证putIfAbsent的原子操作
public class Demo{public List list = Collections.synchronizedList(new ArrayList());public synchronized booleanputIfAbsent(E x){boolean absent = !list.contains(x);if(absent){
list.add(x);
}returnabsent;
}
}
正确的加锁方式:必须使list在实现客户端加锁或外部加锁时使用同一个锁。
/*** 客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端代码。
* 因此要使用客户端加锁,你必须知道对象X使用的是哪一个锁。*/
public class Demo{public List list = Collections.synchronizedList(new ArrayList());public booleanputIfAbsent(E x){synchronized(list){boolean absent = !list.contains(x);if(absent){
list.add(x);
}returnabsent;
}
}
}
通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。然而,客户端加锁却更加脆弱,因为它类C的加锁代码放到与C完全无关的其他类中。在那些并不承诺遵循加锁策略的类上使用客户端加锁需要格外小心。客户端加锁与扩展类机制有许多共同点,都将派生类的行为与基类的失效耦合在一起,这破坏了同步策略的封装性。
4. 更好的方式:组合
/*** ExtendList通过将List对象的操作委托给底层的List实例来实现,同时添加一个原子的putIfAbsent方法。
* 其实这里同样使用了Java监视器模式来封装现有的list,ExtendList通过自身的内置锁增加了一层额外的锁。
* 不需要再关心底层List是否线程安全,即使List不是线程安全或修改了加锁的实现,ExtendList也能提供一致的加锁机制来实现线程安全性。
* 虽然额外的同步层可能导致轻微的性能损失,但与模仿另一个对象的加锁策略相比,则更为健壮。
*@param*/
public class ExtendList implements List{private final Listlist;public ExtendList(Listlist){this.list =list;
}public synchronized booleanputIfAbsent(E x){boolean contains =list.contains(x);if(contains){
list.add(x);
}return !contains;
}//类似方式委托list的其他方法
}