前面我们通过线程安全性和并发之对象的共享大概了解了线程安全和同步的一些基本知识。然而我们并不希望每一次内存访问都进行分析以确保是线程安全的。本章介绍一些组合模式,通过它们我们更容易构建和维护一个线程安全的类。
设计线程安全的类
三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
同步策略定义了如何在不违背对象不变条件(就是对状态的约束,状态值不能随意变换必须符合业务规则)或者后验条件(针对方法,就是在方法执行之后必须为真的条件,如对数据库的操作只有成功了才返回结果)的情况下对其的访问操作进行协同。同步策略规定了如何将不变性、线程封闭加锁机制等结合起来维护线程的安全性,并且还规定了那些变量由那些锁来保护。所以最好有个文档来将同步策略做个规定。
收集同步要求
要确保类的线程安全,就需要确保它的不变性条件不会在线程同步时被破坏,所以需要对其状态进行判断(取值范围等)。
由于不变性条件以及后验条件在状态及状态转换上施加的约束,因此需要额外的同步和封装。
依赖状态的操作
依赖状态的操作以状态的先验条件为基础的操作,例如两个数相除之前要判断分母是否为空,如果为空就不能进行。
在java多线程环境中,等待某个条件为真的各种内置机制(包括等待和通知机制)都与内置加锁机制紧密关联,因为涉及阻塞等行为所以实现起来并不容易,可以通过借助库中的类来实现依赖状态的行为。
状态所有权
对象封装它拥有的状态,即拥有所有权,状态变量的所有者将决定使用那种锁来对它进行保护。如果发布了某个可变对象的引用,那么就不在拥有独占控制权。对于传进函数的对象,类并不拥有这些对象,除非函数有特殊用途。
容器类通常表现出“所有权分离”的形式,容器只拥有其自身的状态,客户代码则拥有容器中各个对象的状态。
实例封闭
如果某对象不是线程安全的,那么可以通过多种技术(可以加锁等)使其在多线程程序中安全的使用。
封装简化了线程安全的实现过程,它提供了一种实例封闭机制。当一个对象被封装到另一个对象中时,能够访问被封装的所有代码路径已知,更易于对代码进行分析。通过封闭机制与合适的加锁机制结合起来,可以确保以线程安全的方式来使用非线程安全的对象。
被封闭对象一定不能超过它既定的作用区域。对象可以封闭在类的一个实例(例如作为类的私有成员)中或者作为某个方法的局部变量或者作为某个方法的返回值。示例如下:
import java.util.HashSet;
import java.util.Set;
public class UserSet{
private final Set<User> myset=new HashSet<User>();
public synchronized void addUserSet(User user){
myset.add(name);
}
public synchronized void removeUserSet(User user){
myset.remove(name);
}
...
}
UserSet只有一个状态myset用了fianl修饰说明你不能获得这个引用也不能把其他引用赋值给它,想要对myset内容进行改变只能通过addUserSet、removeUserSet等方法来进行,而且方法还用synchronized修饰保证同步,所以该类是线程安全的。
注意到被封装在Userset中的User对象如果是可变的也需要进行同步控制。
监视器模式
java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要至始至终都使用该锁对象,都可以用来保护对象的状态。如下:
public class PrivateLock {
private final Object mylock = new Object();
private String s;
public void setS(String s) {
synchronized (mylock) {
this.s = s;
}
}
}
上面使用了私有成员来保护对象状态,这样有什么好处呢?私有的锁对象可以将锁封装起来,时客户代码不那么容易获得锁,但是想参与到同步策略中可以通过公有方法来访问。此外要想验证某个公有锁在程序中是否使用正确需要检查整个程序而不是类。
线程安全性的委托
讲线程安全性的委托之前我们先看个例子:
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;
}
}
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class MutablePointTracker {
private final Map<String, MutablePoint> locations;
public MutablePointTracker(Map<String, MutablePoint> locations) {
this.locations = deppyCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deppyCopy(locations);
}
public synchronized MutablePoint getLocation(String id){
MutablePoint loc=locations.get(id);
return loc==null?null:new MutablePoint(loc);
}
public synchronized void setLocation(String id,int x,int y){
MutablePoint loc=locations.get(id);
loc.x=x;
loc.y=y;
}
private static Map<String, MutablePoint> deppyCopy(Map<String, MutablePoint> locations) {
Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
for (String key : locations.keySet()) {
result.put(key, locations.get(key));
}
return Collections.unmodifiableMap(result);
}
}
可以看出MutablePoint并不是一个线程安全的类,但是MutablePointTracker是,那么MutablePointTracker是怎么保证线程安全的呢?首先看到它只有一个状态locations被final修饰并没有初始,初始化是通过构造函数来完成的,主要是通过入参传给deepCopy()进行深拷贝,并没有直接把入参“赋值”给locations,因为这样是不安全的,入参可能会改变这样locations也会改变,还有入参也要进行同步协调因为拷贝不是原子性的,在拷贝期间可能会被修改内容等。还有getLocaltion获得的只是复制后的内容并没有把原来的MutablePoint对象发布出去,因为可能导致对象逸出。
总体来说该类在线程安全方面做的还是不错的,但是效率可能并不高,因为很多方法涉及拷贝特别是拷贝locations可能需要大量时间,还有deepCopy是静态的只有一份,设计的并不好。