对象组合的意义
- 将一些现有的线程安全组件组合为更大规模的组件或程序,降低线程安全分析和维护的复杂度,提高线程安全的可靠性
设计线程安全类的三要素
- 找出构成对象状态的所有变量
- 找出约束对象状态变量的不变性条件
- 建立对象状态的并发访问管理策略
对象状态
- 对象所有域都是基本类型,那么这些域就构成对象的全部状态
- 对象域中引用了其他对象,那么该对象的全部状态将包含被引用对象的域
同步策略
- 定义:定义了如何在不违背对象的不变性条件和后验条件的情况下对其状态的访问操作进行协同
- 要求:要确保开发人员可以对一个线程安全的类进行分析与维护,就必须将同步策略写成正式文档
不变性条件和后验条件约束的内容
- 不变性条件:约束了有效的状态
- 注意:如果一个不变性条件中包含多个变量,那么在执行任何访问相关变量的操作时,都必须持有保护这些变量的锁
- 后验条件:约束有效的状态转换动作(例如自增操作i++,后一个值必须是前一个值加1)
依赖状态的操作
- 例如:不能从空队列中移除一个元素,在删除元素前,队列必须处于“非空的”状态
- 如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖状态的操作
实例封闭
- 概念:将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁
- 应用:通过将封闭机制与合适的加锁锁机制结合起来,可以确保以线程安全的方式来使用非线程安全的对象
- 作用域:
- 封闭在类的一个实例中(例如,作为类的一个私有成员)
- 封闭在某个作用于内(例如,作为一个局部变量)
- 封闭在线程内(例如,使用ThreadLocal把变量封闭在线程内)
- 实例封闭是实现线程安全类的一个最简单方式
- 实例封闭的优点:
- 在锁策略的选择上有了更多的灵活性,只要自始至终都是用同一个锁,就可以保护状态一致性
- 使得不同状态变量可以由不同的锁来保护
/**
* 该类确保了mySet引用对象的线程安全性
* 但是Person的线程安全性不能保证
* 要保证Person类的线程安全性,必须使Person成为线程安全的类
*/
public class PersonSet {
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);
}
}
构造线程安全类的两种方式
- Java监视器模式
- 线程安全性委托
Java监视器模式
- 概念:Java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用该锁来控制对对象状态的访问,都可以用来保护对象的状态
- 主要优势:简单性
私有锁优点
- 私有锁可以将锁封装起来,客户端代码无法得到锁,但是客户端可以通过公有方法来访问锁,以便参与到它的同步策略中
- 判断锁在程序中是否被正确的使用,私有锁只需要检查单个类,而公有锁需要检查整个应用程序
/**
* 私有锁示例
*/
public class PrivateLock {
private final Object myLock = new Object();
Widget widget;
void someMethod() {
synchronized(myLock) {
//访问或修改Widget的状态
}
}
}
Java线程监视器模式示例
/**
* Created by weicm on 2017/8/9.
* 可变位置,线程不安全
*/
public class MutablePoint {
public int x, y;
public MutablePoint() {
x = 0;
y = 0;
}
/**
* 位置对象拷贝
* @param p
*/
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Created by weicm on 2017/8/9.
* 基于Java监视器的线程安全的车辆追踪器
*
* 介绍:
* 线程安全实现方式:这种实现方式是通过在返回给客户端代码之前复制可变数据来维持线程安全性
* 缺点:如果车辆容器太大,getLocations会由与deepCopy会导致低性能
* 实现方式的好坏:如果结果需要保持一致性,那么这种方式很适合,如果结果要保持实时性,那么这种方式不符合要求
*/
public class MonitorVehicleTracker {
//追踪器状态
private final Map<String, MutablePoint> locations;
/**
* 通过深度拷贝传入的追踪器信息,初始化追踪器状态,避免追踪器状态逸出
* @param locations
*/
public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations);
}
/**
* 返回只读追踪器镜像,避免追踪器状态逸出
* @return
*/
public synchronized Map<String, MutablePoint> getLocations() {
return deepCopy(locations);
}
/**
* 获取位置镜像,避免位置状态对象逸出
* @param id
* @return
*/
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
/**
* 通过深度拷贝追踪器创建器镜像,返回只读镜像
* @param m
* @return
*/
private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
HashMap<String, MutablePoint> result = new HashMap<>();
for (String id: m.keySet())
result.put(id, new MutablePoint(m.get(id)));
return Collections.unmodifiableMap(result);
}
}
线程安全性委托示例
/**
* Created by weicm on 2017/8/10.
* 不可变位置,线程安全
*/
public class Point {
public final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by weicm on 2017/8/10.
* 基于安全委托的线程安全的车辆追踪器
*/
public class DelegatingVehicleTracker {
//追踪器状态
private final ConcurrentHashMap<String, Point> locations;
//追踪器 实时 只读 对象(并不是状态对象,只做为应答结果,由与是只读的,所以安全)
private final Map<String, Point> unmodifiableMap;
/**
* 初始化,将状态对象委托给线程安全的类
* 同时,构造实时只读状态对象
* @param locations
*/
public DelegatingVehicleTracker(Map<String, Point> locations) {
this.locations = new ConcurrentHashMap<>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}
/**
* 返回实时只读状态对象,安全
* @return
*/
public Map<String, Point> getLocations() {
return unmodifiableMap;
}
/**
* 通过委托的线程安全对象,获取不可变位置状态,安全
* @param id
* @return
*/
public Point getLocation(String id) {
return locations.get(id);
}
/**
* 通过委托的线程安全对象,设置新的不可变位置状态,安全
* @param id
* @param x
* @param y
*/
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);
}
}
}
委托多个线程安全的状态变量
发布底层状态变量
- 如果一个状态变量是线程安全的,并且没有任何不变性条件来约束他的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量
/**
* Created by weicm on 2017/8/10.
* 线程安全且可变的SafePoint类
*/
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;
}
}
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by weicm on 2017/8/10.
* 安全发布底层状态的车辆追踪器
*/
public class PublishingVehicleTracker {
//追踪器状态
private final Map<String, SafePoint> locations;
//追踪器 实时 只读 对象(并不是状态对象,只做为应答结果,由与是只读的,所以安全)
private final Map<String, SafePoint> unmodifiableMap;
/**
* 初始化,将状态对象委托给线程安全的类
* 同时,构造实时只读状态对象
* @param locations
*/
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}
/**
* 返回实时只读状态对象,安全
同时也发布了unmodifiableMap中的SafePoint对象,由与SafePoint对象是线程安全的,所以可以安全的发布,原因同getLocation方法
* @return
*/
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
/**
* 通过委托的线程安全对象,获取不可变位置状态,安全
* 发布了底层状态对象SafePoint,由于以下原因,所以PublishingVehicleTracker仍然是线程安全的
* 1. 由于SafePoint是线程安全的
* 2. 没有不变性条件约束SafePoint的有效值
* 3. SafePoint只有set一个状态转换,不存在不允许的状态转换
* @param id
* @return
*/
public SafePoint getLocation(String id) {
return locations.get(id);
}
/**
* 通过委托的线程安全对象,设置新的不可变位置状态,安全
* @param id
* @param x
* @param y
*/
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);
}
}
在现有的线程安全类中添加功能的方法
- 修改原始类(最安全,需要了解类的同步策略)
- 扩展类(较修改原始类更脆弱,因为同步策略分布在多个相关的单独维护的源代码中)
- 客户端加锁(较扩展类更脆弱,因为同步策略分布在多个不相关的单独维护的源代码中)
- 组合,将类的操作封装起来,用组合类的内置锁来实现同步策略,而不关心被封装的类是否是线程安全的(安全,由与封装对象可能是线程安全的,所以可能有轻微性能损失)
良好的习惯
- 在维护线程安全性时,文档是最强大的工具之一;在文档中说明客户端需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略