第四章 组合对象
4.1 设计线程安全的类
在没有进行全局检查的情况下,封装能保证线程的安全性。
设计线程安全类的过程包括一下三个基本要素:
- 1确认对象的状态由哪些变量构成的
- 2 确定限制状态变量的不变约束
- 3 制定一个管理并发访问对象状态的策略
- 同步策略定义了对象如何协调对其状态的访问,并且不会违反它的不变约束或后验条。
4.1.1 收集同步需求
维护类的线程安全就意味着确保在并发访问的情况下,保护它的不变约束,这需要对其状态进行判断。对象与变量拥有一个状态空间:即它们可能处于状态的范围。状态空间越小,越容易判断它们。尽量使用final类型的域,可以简化我们的判断。
很多类可以通过不变约束来判定一中状态是合法的还是非法的。
一个类的不变约束也可以约束多个状态变量。当不变约束涉及多个变量的时候,任何一个操作在访问相关变量期间,线程必须占有保护这些变量的锁。
4.1.2 状态依赖的操作
若一个操作存在基于状态的先验条件,则称它为状态依赖的。
在单线程,如果操作无法满足状态依赖,则必然失败。但是在并发程序中,原本为假的先验条件可能会由于其他线程的活动而变成真。并发程序有这种可能:持续等待,直到先验条件为真,在继续处理操作。
在java中,等待特定条件成立的内置高效机制:notify和wait 与内部锁紧密的绑定在一起,因为想正确的使用它们并不容易。
创建一个条件,让它执行前必须等待先验条件为真,不如使用现有类库来提供期望的状态依赖行为更容易,比如阻塞队列或信号量,以及其他同步工具。
4.1.3 状态所有权
所有权意味着控制权。
很多情况下:所有权和封装性总是在一起出现的:
- 对象封装它有的状态;
- 拥有它封装的状态;
4.2 实例限制
- 将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据的时候纵能获得正确的锁。
通过使用实例控制,封装简化了类的线程安全工作,这通常称之为限制。
被限制的对象一定不能逸出到它的期望可用范围之外,可以把对象限制在类实例(比如私有的类成员) 语法范围(本地变量) 或线程(对象在线程内部从一个方法传递到另一个方法,前提是该对象不被跨线程共享)中,对象不会自发的溢出自己。
- 如下代码,非线程安全的myset管理者personset的状态,但是myset是私有的,不会溢出,想访问只能通过personset这个类的addPerson和containsPerson来访问,此时需要获得PersonSet的锁,PsersonSet的内置锁保护了它所有的状态,因而确保了PersonSet是线程安全的:
public class PersonSet {
@GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
interface Person {
}
}
- 实例构建是构建线程安全最简单的方法之一。允许不同的所保护不同的状态变量。
- 发布其他对象,比如迭代器中或者内部类实例,可能会间接地发布受限对象,这样受限对象同样会溢出。
4.2.1 java 监视器模式
线程限制的直接推论之一就是java监视器模式 , 遵循java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。
私有锁保护状态:使用私有锁对象,而不是对象的内部锁,有很多好处:比如私有对象的锁可以封装锁,这样客户代码无法获得它。
示例代码如下:
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock") Widget widget;
void someMethod() {
synchronized (myLock) {
// Access or modify the state of widget
}
}
}
4.2.2 范例:机动车追踪器
每个机动车都有一个String标识,一个与之对应的位置(x,y),每个VehicleTracker对象都封装了一辆已知机动车的标识(identity)和位置(localtinon)
Map<String,Point> locations = vehicles.getLocaltions();
for(String key :locations.keySet() ){
renderVechicle(key,localtion.getKey());
}
类似的,更新线程会在GPS设备上获取最新的数据或者手动修改数据修改机车的位置
void vehicleMoved(VehicleMovedEvent evt){
Point loc = evt.getNewLocaltion();
vehicles.setLocaltion(evt.getVehicleId(),loc.x,loc.y);
}
因为视图线程和更新线程是会并发的访问数据模型,因此模型必须是线程安全的。
java监听器机动车追踪器的实现:
public class MonitorVehicleTracker {
@GuardedBy("this") private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(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);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.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)));
return Collections.unmodifiableMap(result);
}
}
更新线程MutablePoint描述机车位置:
@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;
}
}
虽然这里的MutablePoint 不是线程安全的,但是MonitorVehicleTracker类是的。
我们需要将机车的位置数据返回给调用者时,正确的返回值是从MutablePoint执行拷贝的构造函数或者deepCopy 方法拷贝出来的,deepCopy会创建一个新的map。数据量小的时候性能问题不会太大,多了的话会影响性能。这个也会造成一个新的问题,那就是即使真是的locations已经发生改变,返回的拷贝的数据仍然没有变化。