1、摘要
虽然知道了如何去编写线程安全的类,但是,我们不希望每一次都从底层的类开始写。
我们如何能够使用现有的线程安全类来组合为更大规模的组件或程序呢?
如何用不是线程安全的类来组合构建我们的线程安全类呢?
2、设计线程安全的类
在设计线程安全类时,需要包含以下三个要素: 1、找出构成对象状态的所有变量 分析对象的域,对象的状态就是这些域构成的n元组。 2、找出约束状态变量的不变性条件 不变性条件:即状态变量应满足的基本条件(值域等),一定不能违背! 3、建立对象状态的并发访问管理策略 同步策略定义了如何在不违反变量的不变性条件或后验条件下对变量状态的访问操作进行协同。
先验条件:即在操作发生之前必须满足的条件(如除数不能为0);
后验条件:即在操作发生后(操作的结果)必须满足的条件(如 1+2必须等于3)。
状态迁移的有效性:必须从一个有效状态切换到另一个有效状态(如counter当前值为1,下一个值必须为2)。
由于不变性条件以及后验条件在状态及状态迁移上施加了各种约束,
因此,就必须要进行额外的同步与封装,以确保对象不会处于无效状态。
如果一个操作中存在无效的状态转换,那么该操作必须是原子的。
如果不了解对象的不变性条件和后验条件,就不能确保线程安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助原子性与封装性。
3、实例封闭
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制,
当一个对象被封闭在另一个对象中,能够访问被封装对象的所有代码路径都是已知的(getter、setter及其他方法)。
将数据封装在对象内部,可以将对象的访问限制在方法上,从而更容易确保线程在访问数据时持有正确的锁。
封装机制更容易构造线程安全的类,因为封装类的状态时,分析类的线程安全性不需要检查整个类。只需要检查这个类对外的接口。
不能破坏封装!被封闭的对象一定不能超出它们既定的作用域。要发布封装对象状态,必须安全发布。
一个使用实例封闭的例子:
myPersonSet 被封装在了PersonSet里面,对于myPersonSet状态进行访问的路径都做了同步处理。
@NotThreadSafe class Person{} @ThreadSafeclass PersonSet{ //final保证了构造过程的安全性 private final Set<Person> myPersonSet=new HashSet<>(); public synchronized void addPerson(Person person){ myPersonSet.add(person); } public synchronized boolean containPerson(Person person){ return myPersonSet.contains(person); } }
4、Java监视器模式
Java监视器模式只是一种编写代码的约定。
遵循监视器模式的对象会将所有的可变状态都封装起来,并由自己的内置锁来保护。
/** * 使用私有的锁,能够让客户端代码不能直接获取到锁 * 只能访问公有方法来访问锁 */ class PrivateLock{ class Widget{} //使用私有的锁并将其封装起来 private final Object myLock=new Object(); @GardedBy("myLock") private Widget widget; void someMethod(){ synchronized (myLock){ //访问或修改widget状态 } } }
监视器模式的一个例子:车辆追踪器
虽然MutablePoint是不安全的,但是通过将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; } } @ThreadSafe 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); } }
5、线程安全性的委托
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效转换状态,那么就可以将线程安全性委托给底层的状态变量。
如果一个类的状态变量是一组彼此独立的变量,
即它们没有一起构成不变性条件,组合而成的类不会在这些状态变量上增加任何的不变性条件。
那么,这个类的线程安全就可以委托给这些状态变量来实现,
只要这些状态变量是线程安全的,那么这个类就可以不用额外的同步而实现线程安全。
下面这个例子,委托给一个状态变量:
委托给了 ConcurrentMap,因为ConcurrentMap是线程安全的,因此该类不需要额外的同步。
同时,getLocations() 每次返回的都是最新的实时数据。
@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 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);
/*
Returns an unmodifiable view of the specified map.
This method allows modules to provide users with "read-only" access to internal maps.
Query operations on the returned map "read through" to the specified map,
and attempts to modify the returned map, whether direct or via its collection views,
result in an UnsupportedOperationException.
*/ } 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); } }
下面这个例子,将线程安全委托给了多个状态变量:
因为各个状态变量是独立的,而且它们又是线程安全的,因此不需要额外同步。
public class VisualComponent { private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<KeyListener>(); private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) { keyListeners.add(listener); } public void addMouseListener(MouseListener listener) { mouseListeners.add(listener); } public void removeKeyListener(KeyListener listener) { keyListeners.remove(listener); } public void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener); } }
当然,委托也是有前提的:各个状态变量必须相互独立,它们不会多个一起构成不变性条件。
但是,大多数类都不会这么简单。
下面这个例子就是委托失效的例子:
lower和upper一起构成了lower<=upper的不变性约束。
这时,需要在复合操作处增加额外同步。
public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Warning -- unsafe check-then-act if (i > upper.get()) throw new IllegalArgumentException( "can't set lower to " + i + " > upper"); lower.set(i); } public void setUpper(int i) { // Warning -- unsafe check-then-act 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()); } }
6、发布底层的状态变量
当把线程安全性委托给某个对象的底层状态变量时,在什么时候才可以发布这些变量从而使其他类能够修改它们?
答案取决于在类中对这个变量施加了那些不变性条件。
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量上的操作也不存在任何不允许的状态转换,那么就可以安全的发布它。
我们必须保证对发布出去的对象的修改不会使得我们的类无效。
这时候,封装的作用就出来了,将不变性约束封装在类中,
使得外部对对象的修改都只能通过公有方法来完成,从而保证修改不会违背不变性条件。
一个发布状态的例子:
Point对象随着Map对象的发布而被发布了。
但是,Point是线程安全的,并且内部封装了不变性约束,因此可以安全发布。
@ThreadSafe public 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 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); } }
7、在现有的线程安全类中添加功能
1、直接修改原始的类
最安全的方法。
但是,想Peach呢,你改得到?
2、继承现有类,并添加功能
将同步策略实现分布到了多个单独维护的源代码文件中,
如果底层的类改变了同步策略并选择了不同的锁来保护状态,那么子类就被破坏了。
下面这个例子通过继承扩展了Vector类。
@ThreadSafe public class BetterVector<E> extends Vector<E> { public synchronized boolean putIfAbsent(E x) { boolean absent = !contains(x); if (absent) add(x); return absent; } }
3、客户端加锁机制
不扩展类本身,而只是扩展它的功能。
使用一个“辅助类”,将扩展代码放入辅助类中(只添加了扩展代码)。
客户端加锁机制和通过继承扩展机制一样,都派生类的行为与基类的实现耦合在了一起,
都会破坏同步策略的封装性。
下面是一个错误例子:
扩展时必须添加正确的锁!
ListHelper类中使用的锁是它的内置锁,肯定和List使用的锁不同!
如果在扩展功能时使用了不同于原始类的锁,那么我们添加的操作对于原始类原有的操作来说不是原子的!
因此,我们需要知道原始类是由哪一个锁保护的,并使用相同的锁。
@NotThreadSafe 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; } }
正确的添加方式为:
使用List的内置锁。
@ThreadSafe 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; } } }
4、组合
使用代理的方式,代理原始类的所有方法。
这时,可以不用管原始类使用的什么锁。
@ThreadSafe 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(); } // ... similarly delegate other List methods }
8、将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
在进行软件开发时,良好的文档是减轻维护成本的最好方式。
在文档中,要指明类是否是线程安全的,它使用了什么锁?
如果一个类没有明确地声明是线程安全的,那么就不能假设它是线程安全的。
猜测接口的线程安全性时,应该从实现者的角度去考虑,而不是使用者的角度。