文章目录
设计线程安全的类
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
找出构成对象状态的所有变量
分析对象的状态,首先从对象的域开始:
- 如果对象所有域都是基本类型的变量,那么这些域构成对象的全部状态。对于含有n个基本类型域的对象,其状态就是这些域构成的n元组。
- 如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。
示例
public class Person{
private int id;
}
这个对象的域为id,这个域就是Person对象的全部状态。
public class Coordinate{
private int x;
private int y;
}
这个对象有两个域x,y;Coordinate对象的状态为二元组(x,y);
public class Diary{
private Person person;
}
这个对象有两个域person,这个域都是引用类型。所以状态包含person对象里的域id.
找出约束状态变量的不变性条件
利用不变性条件判断状态是否有效,例如整型int类型,状态空间为Integer.MIN_VALUE~Integer.MAX_VALUE
后验条件判断迁移是否有效。如果在某个操作中存在无效的状态转换,那么该操作必须是原子的。
实例封闭
如果某对象不是线程安全的,一般可以通过两种技术使其在多线程程序中安全的使用:
- 确保该对象只能由单个线程访问(线程封闭)
- 通过一个锁来保护该对象的所有访问。
public class PersonSet{
private final Set<Person> mySet = new HashSet<>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
这个类是线程安全的,虽然HashSet是非线程安全的,但是mySet是私有的,且不会被get,因此HashSet是被封闭在PersonSet中的。而且能访问mySet的addPerson和containsPerson在执行时需要访问PersonSet的内置锁,因此PersonSet是个线程安全的类。需要注意的是这里的Person类如果是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步。
java监视器模式
遵循java监视器模式的对象会把所有可变状态都封装起来,并由对象的内置锁保护。
私有的锁对象:
//线程安全
public class PrivateLock{
private Object myLock = new Object();
Widget widget;
void someMethod() {
synchronized(myLock) {
// 访问或修改Widget的状态
}
}
}
使用私有的锁对象而不是对象的内置锁,有以下优点:
- 私有的锁对象可以将锁封装起来,使客户代码无法获得锁,但客户代码可以通过公有方法来访问锁,以便参与到它的同步策略中。
示例:车辆追踪
public class MonitorVehicleTracker {
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 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> res = new HashMap<String,MutablePoint>();
for(String id : m.keySet()) {
res.put(id,new MutablePoint(m.get(id)));
}
return Collections.unmodifiableMap(res);
}
// 这个类非线程安全
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;
}
}
}
线程安全性的委托
public class CountingFactorizer{
public AtomicLong value;
}
对于这样一个类,由于AtomicLong是线程安全的,而且CountingFactorizer只包含value一个状态,所以CountingFactorizer是线程安全的。我们将这个过程称为线程安全性的委托,CountingFactorizer将它的线程安全性委托给AtomicLong保证。
示例:基于委托的车辆追踪器
public class DelegatingVehicleTracker {
private final ConcurrentMap<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 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);
}
}
}
class Point {
public final int x,y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
}
如果使用最初的MutablePoint而不是Point类,就会破坏封装性,因为getLocations会发布一个指向可变状态的引用,而这个引用不是线程安全的。
委托给多个独立的状态变量
前面的委托都是针对单个状态变量,我们还可以将线程安全性委托给多个状态变量。
public class VisualComponent {
private final List<KeyBoardListener> keyBoardListener = new CopyOnWriteArrayList<KeyBoardListener>();
private final List<MouseListener> keyBoardListener = new CopyOnWriteArrayList<MouseListener>();
public void addKeyBoardListener(KeyBoardListener listener) {
keyBoardListener.add(listener);
}
public void addMouseListener(MouseListener listener) {
MouseListener.add(listener);
}
public void removeKeyBoardListener(KeyBoardListener listener) {
keyBoardListener.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
MouseListener.remove(listener);
}
}
委托失效
当有约束条件时,委托容易失效
public class NumberRange{
// 不变性条件:lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
if(i > upper.get()) {
throw new IllegalArgumentException("can't set low to "+ i + " > upper");
}
lower.set(i);
}
public void setUpper(int i) {
if(i < lower.get()) {
throw new IllegalArgumentException("can't set upper to "+ i + " < lower");
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
NumberRange不是线程安全的,没有维持对下界和上界进行约束的不变性条件。
setLower 和 setUpper都尝试维持不变性,但是都没有做到,因为他们是采用“先检查后执行”的操作,但没有使用足够的加锁机制保证这些操作的原子性。如果一个线程调用setLower(5),另一个线程调用setUpper(4),如果执行时序错误,那么两个方法同时通过检查进行设置就会导致upper<lower。不符合约束。
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
发布底层的状态变量
根据类对底层状态变量施加的不变性条件,我们才可以发布这些变量从而使其他类能修改他们。
如果一个状态变量是线程安全的,并且没有任何不变性条件约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。
示例:发布状态的车辆追踪器
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 getLocation(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);
}
}
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;
}
}
SafePoint(SafePoint p)获取p的拷贝,方法体内不直接调用SafePoint(int x,int y)的原因是因为避免产生竞态条件,私有构造函数可以避免这种竞态条件。(私有构造函数捕获模式)
PublishingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,不同的是Map中的元素是线程安全且可变的Safeoint,而并非不可变的。getLocation返回底层Map对象的一个不可变副本。调用者可以修改Map中的SafePoint值改变车辆的位置。
在现有的线程安全类中添加功能
假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加”操作。
因为需要的是一个线程安全的类,那么这种“先查询后执行”的操作就要是原子的.
最安全的方式是修改原始的类,但这通常无法做到。另一个方法就是拓展这个类,但是并非所有的类都像Vector这样将状态像子类公开。
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if(absent) {
add(x);
}
return absent;
}
}
客户端加锁机制
对于由Collections.synchronizedList封装的ArrayList,在原始类中添加一个方法或者对类进行扩展都不行,第三种策略是扩展类的功能,但不是扩展类本身,而是将扩展代码放入一个“辅助类”中。
一个错误的做法:
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if(absent) {
list.add(x);
}
return absent;
}
}
错误的原因是synchronized没有锁住list,在执行putIfAbsent时可能list会被另外一个线程修改。
如果想要正确执行该方法,必须使List在实现客户端加锁或外部加锁时使用同一个锁。
客户端加锁是值,对使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。在这里X指的是list。
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized(list) {
boolean absent = !list.contains(x);
if(absent) {
list.add(x);
}
return absent;
}
}
}
通过添加一个原子操作扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。客户端加锁却更加脆弱因为它将类X的加锁代码放到和X完全无关的其他类中。
组合
组合是一种更好的方法。
public class ImprovedList<T> implements List<T> {
private final List<T> list;
public ImprovedList(List<T> 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();
}
}
通过自身的内置锁增加了一层额外的加锁。我们使用了java监视器模式来封装现有List,并且只要在类中拥有指向底部List的唯一外部引用,就能确保线程安全性。
将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
在设计同步策略时需要考虑多个方面:例如,将哪些变量声明为volatile类型,哪些变量用锁来保护,哪些锁保护哪些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作必须是原子操作等。