1.设计线程安全的类
我们通过封装技术,可以在不对整个程序进行分析的情况下判断一个类是否线程安全。
在设计线程安全类的过程中,需要包含以下三个基本要素:
**找出构成对象状态的所有变量
**找出约束状态变量的不变形条件
**建立对象状态的并发访问管理策略
同步策略定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。
(1)收集同步需求
要确保类的安全性,就要确保它的不变性条件不会再并发访问的情况下被破坏,这需要对其状态进行推断。同样,在操作中还会包含一些后验条件来判断状态迁移是否有效。
由于不变性条件以及后验条件的约束,就需要额外的同步和封装。
如果不了解对象的不变性条件和后验条件,那么就不能确保线程安全性,要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性和封装性。
(2)依赖状态的操作
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的,但是如果在某个操作中包含有基于状态的先验条件,那么这个操作就被称为依赖状态的操作。
在java中,等待某个条件为真的各种内置机制都与内置加锁机制紧密关联,要想正确使用并不容易。我们可以通过现有库中的列来实现依赖状态的行为。比如阻塞类以及其他的同步工具类,和在平台与类库中提供的各种底层机制来创建依赖状态的类
(3)状态的所有权
所有权属于类设计中的一个要素。
所有权和封装性是相互关联的:因为封装了对象,所以拥有了对象对应的状态。所有权意味着控制权。
当发布了某个可变对象的引用,那么就不再独占所有权, 最多是共享所有权。
容器类会表现出“所有权分离”的现象:容器类本身有状态,而容器类里的对象也有状态,这二者的状态时分离的,各玩各的。
2.实例封闭
如果某对象不是线程安全的,可以通过多种技术使其在多线程中安全使用。可以确保该对象只能由单个线程访问(线程封闭),也可以通过锁来保护该对象的所有访问。
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
被封闭对象一定不能超出它们既定的域,即不能逸出。当然,一般逸出也是开发人员发布时使其逸出的。
示例:
@ThreadSafe
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);
}
}
此处PersonSet是由HashSet管理,不是线程安全的,但是由于myset是私有且没有逸出,因此Hashset被封闭在PersonSet中。而唯一能访问myset的代码是addPerson和containsPerson,它们都获得了PersonSet的锁。PersonSet的状态完全由它的内置锁保护,因此PersonSet是线程安全的。
但是如果,Person类是可变的,在从PersonSet获取Person对象时还需要额外同步。最可靠的方法是让Person成为一个线程安全的类。当然,也可以用锁来保护Person对象。
实例封闭是构件线程安全列的一个最简单方式,它使锁策略变得更加灵活。实例封闭还使得不同的状态变量可以由不同的锁来保护。
java平台类库提供很多线程封闭的示例,他们能将非线程安全的类转化为线程安全的类。
类库还提供包装器工厂方法。这些工厂方法通过装饰器模式将容器类封装到一个同步的包装器对象中,包装器对象能将方法都变成同步方法,然后将同步方法转发去调用底层的容器对象。从而保证非线程安全的类可以在多线程环境中安全的使用。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性就不需要检查整个程序。
(1)Java监视器模式
遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。当然也可以通过私有锁保护状态。
示例1:通过私有锁保护状态
/**
* 通过私有锁保护状态
*/
public class PrivateLock {
private final Object myLock = new Object();
@GuardedBy("myLock")
Widget widget;
void someMethod(){
synchronized (myLock){
//修改或访问widget的状态
}
}
}
示例2:车辆追踪——车辆由位置更新线程和视图线程共享
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;
}
}
/**
* 基于监视器模式的车辆追踪
*/
@ThreadSafe
public class MonitorVehicleTracker {
@GuardedBy("this")
private final Map<String,MutablePoint> locations;
public MonitorVehicleTracker(Map<String,MutablePoint> locations){
this.locations = locations;
}
public synchronized Map<String,MutablePoint> getLocations(){
return deepCopy(locations);
}
public synchronized MutablePoint getLocations(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 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);
}
}
监视器模式获取的不是最新的,类似于快照
3.线程安全性的委托
当从头开始构件一个类,或者将多个非线程安全的类组合为一个类时,用java监视器模式非常有用。
但是想一下,当类中的各个组件已经是线程安全的了,我们是否还需要一个额外的线程安全层?
我们可以将类的线程安全性委托给内部的一个变量。
(1)基于委托的车辆追踪器
/**
* 不可变的point类
*/
@Immutable
public class Point {
public final int x,y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
}
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentHashMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String,Point> points){
locations = new ConcurrentHashMap<String,Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String,Point> getLocations(){
return unmodifiableMap;
}
public Point getLocations(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);
}
}
}
委托模式获取的是实时的位置
(2)独立的状态变量
上述委托只是委托给了单个线程安全的状态变量。我们还可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的。
示例:鼠标键盘监听
public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener keyListener){
keyListeners.add(keyListener);
}
public void addMouseListener(MouseListener mouseListener){
mouseListeners.add(mouseListener);
}
public void removeKeyListener(KeyListener keyListener){
keyListeners.remove(keyListener);
}
public void removeMouseListener(MouseListener mouseListener){
mouseListeners.remove(mouseListener);
}
}
(3)当委托实效时
大部分组合对象不会像上述那样简单,因为它们的状态变量之间可能存在某些不变性条件。
如果某个类含有复合操作,那么仅靠委托并不足以实现安全性,在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。
(4)发布底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束他的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全发布这个变量。
(5)发布状态的车辆追踪器
/**
* 线程安全且可变的point类
*/
@ThreadSafe
class SafePoint {
@GuardedBy("this")
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;
}
}
/**
* 安全发布底层状态的车辆追踪器
*/
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String,SafePoint> locations){
this.locations = new ConcurrentHashMap<String,SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String,SafePoint> getLocations(){
return unmodifiableMap;
}
public SafePoint getLocations(String id){
return locations.get(id);
}
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);
}
}
线程安全性委托给了ConcurrentHashMap,不能增删车辆,但是可以修改车辆位置。如果x,y有效值施加了约束,那就不是线程安全的,否则就是线程安全的。
4.在现有的线程安全类中添加功能
假设一个线程安全的链表,需要添加一个原子的“若没有则添加”的操作。
要执行这个操作,最安全的方法是修改原始的类。但是一般无法做到。
还有一种方法是拓展这个类,这个方法较为脆弱,因为同步策略实现被分布到多个单独维护的源代码文件中。
(1)客户端加锁机制
第三种策略是拓展类的功能。不是拓展类本身,而是将拓展代码放入到一个“辅助类”中。
@ThreadSafe
public class ListHelper {
public List<Widget> list = Collections.synchronizedList(new ArrayList<Widget>());
public boolean putIfAbsent(Widget widget){
synchronized (list){
boolean absent = !list.contains(widget);
if(absent){
list.add(widget);
}
return absent;
}
}
}
通过一个原子操作来拓展类是脆弱的,因为它将类的加锁代码分布到多个类中,然而,客户端加锁更加脆弱,因为它将类的加锁代码放到了和类完全无关的其他类中。
客户端加锁和拓展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合了。会破坏同步策略的封装性。
(2)组合
为现有的类添加一个原子操作还有一个更好的方法:组合。
@ThreadSafe
public class ImproveList<T> implements List<T> {
private final List<T> list;
public ImproveList(List<T> list){
this.list = list;
}
public synchronized boolean putIfAbsent(T x){
boolean contains = list.contains(x);
if(contains){
list.add(x);
}
return !contains;
}
@Override
public void clear() {
list.clear();
}
//...
}
通过将List对象的操作委托给底层List来实现list的操作,同时还添加了一个原子的方法。
ImproveList通过自身的内置锁增加了一层额外的加锁,即使List不是线程安全的,ImproveList也会提供一致的加锁机制来实现线程安全性。
我们使用java监视器模式来封装现有的list,并且只要在类中拥有指向底层list的唯一外部引用,就能确保线程安全。
5.将同步策略文档化
这一点很重要