在设计线程安全类的过程中,需要包含以下三个基本要素:
如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性于封装性。
如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作。
一种更简单的方法是通过现有库中的类(例如阻塞队列[Blocking Queue]或信号量[Semaphore]来实现依赖状态的行为)。
在Java中,所有权属于类设计中的一个要素。
为了防止多线程在并发访问同一个对象时产生的相互干扰,这些对象应该要么是线程安全的对象,要么是事实不可变的对象。或者由锁来保护的对象。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
//通过封闭机制来确保线程安全
//如果Person类是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。
@ThreadSafe
public class PersonSet{
@GuardedBy("this")
private final Set mySet = new HashSet<>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
public synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}
Java类库提供了包装器工厂方法(例如Collections.synchronizedList以及其它类似方法),使得这些非线程安全的类(例如ArrayList和HashMap)可以在多线程环境中安全地使用。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无须检查整个程序。
遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,由对象自己的内置锁来保护。
// 通过一个私有锁来保护状态
public class PrivateLock{
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod(){
//私有的锁对象可以将锁封装起来,是客户代码无法得到锁
synchronized(myLock){
//访问或修改Widget的状态
}
}
}
@ThreadSafe
public class MonitorVehicleTracker{
@GuardedBy("this")
private final Map locations;
public MonitorVehicleTracker(Map locations){
this.locations = deepCopy(locations);
}
public synchronized Map getLocations(){
return deepCopy(locations);
}
public synchronized void setLocation(String id,int x,int y){
MutablePoint loc = locations.get(id);
if(loc == null)
throw new IllegalArgumentException("No such ID" + id);
loc.x=x;
loc.y=y;
}
private static Map deepCopy(Map m){
Map result = new HashMap();
for(String id:m.keySet())
result.put(id,new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
@NotThreadSafe
public class MutablePoint{
public int x,y;
public MutablePoint(){x=0,y=0;}
public MutablePoint(MutablePoint p){
this.x=p.x;
this.y=p.y;
}
}
当车辆容器非常大的情况下将极大的降低性能。deepCopy将执行较长时间,tracker的内置锁将被占用。造成车辆的实际位置发生了变化,但返回的信息却保持不变。
@Immutable
public class Point{
public final int x,y;
public Point(int x,int y){
this.x=x;
this.y=y;
}
}
/*如果使用MutablePoint而不是Point,就会破坏封装性,
因为getLocations会发布一个指向可变状态的引用,而这个引用不是线程安全的。
*/
@ThreadSafe
public class DelegatingVehicleTracker{
private final ConcurrentMap locations;
private final Map unmodifiableMap;
public DelegatingVehicleTracker(Map points){
locations = new ConcurrentHashMap(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map getLocations(){
return unmodifiableMap;
}
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);
}
//如果需要一个不发生变化的车辆视图,浅拷贝,只复制Map结构,不复制内容
public Map getLocations(){
return Collections.unmodifiableMap(new HashMap(locations));
}
}
组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。
public class VisualComponent{
/*CopyOnWriteArrayList是线程安全的链表
keyListener和mouseListeners彼此独立,VisualComponent可以将其线程安全性委托给这两个线程安全的监听器列表。
*/
private final List keyListeners = new CopyOnWriteArrayList<>();
private final List mouseListeners = new CopyOnWriteArrayList<>();
public void addKeyListener(KeyListener listener){
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener){
mouseListeners.add(listener);
}
}
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
将扩展代码放入一个”辅助类中”。
扩展会破坏实现的封装性
@ThreadSafe public class ListHelp{ public List list= Collections.synchronizedList(new ArrayList ()); /*在putIfAbsent上添加synchronized不能实现线程安全 list上的锁和ListHelp上的锁不是一个锁。 */ public boolean putIfAbsent(E x){ synchronized(list){ boolean absent =!list.contains(x); if (absent) list.add(x); return absent; } } }
当为现有的类添加一个原子操作时,有一种更好的方法:组合。
//通过组合实现"若没有则添加"
@ThreadSafe
public class ImprovedList implements List {
private final List list;
public ImprovedList(List 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 clear() {
list.clear();
}
}
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。