文章目录
本章介绍一些组合模式,使一个类共容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保障.
1.设计线程安全的类
通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的
在设计线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发访问管理策略
分析对象的状态,要从对象的域开始.
@ThreadSafe
public final class Counter {
@GuardedBy("this")
private long value = 0;
public synchronized long getValue() {
return value;
}
public synchronized long increment() {
if (value == Long.MAX_VALUE) {
throw new IllegalStateException("counter overflow");
}
return value ++;
}
}
上例中,Counter
只有一个域value
,因此这个域就是Counter的全部状态.
对于含有n个基本类型域的对象,其状态就是这些域构成的n元组.
如果在对象的域中引用了其它对象,那么该对象的状态将包括被引用对象的域.
1.1 收集同步需求
要确保类的线程安全性,就要确保类的不变性条件不会在并发访问的情况下被破坏.
对象与变量都有一个状态空间,即所有可能的取值.
类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的.
- 不变性条件: 用于判断状态是有效还是无效的.
- 后验条件: 判断状态迁移是否有效.
如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性.要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助原子性与封装性.
1.2 依赖状态的操作
依赖状态的操作:某个操作中包含有基于状态的 先验条件.
在单线程程序中,不满足先验条件程序会失败,但是在多线程中,先验条件可能会由于其它线程执行的操作而变成真.在并发程序中要一直等到先验条件为真然后再执行操作.
2.实例封闭
封装简化了线程安全类的实现过程,它提供了一种实例封闭机制.
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁.
下例中,PersonSet
状态由HashSet
来管理,虽然HashSet
不是线程安全的,但由于mySet
是私有并且不会逸出,唯一能访问mySet
的代码路径是addPerson
与containsPerson
,在执行他们都要获得PersonSet上的锁,所以这是个线程安全的类.
public class PersonSet {
private final Set<Person> mySet = new HashSet<>();
public synchronized void addPerson(Person person) {
mySet.add(person);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
@Data
static class Person {
private String name;
private String age;
}
}
但如果Person类是可变的,则还需要做额外的同步,使Person也成为一个线程安全的类.
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无需检查整个程序
2.1 Java监视器模式
Java监视器模式仅仅是编码的约定,对于任何一种锁,只要自始自终都使用该锁对象,都可以用来保护对象的状态.
上面的Counter
类,是一种用处不大的Java监视器模式示例.
车辆追踪:
@ThreadSafe
public class MonitorVehicleTracker {
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) {
HashMap<String, MutablePoint> result = new HashMap<>();
for (String id : m.keySet()) {
result.put(id, new MutablePoint(m.get(id)));
}
//unmodifiableMap 产生一个只读Map
return Collections.unmodifiableMap(result);
}
}
@NotThreadSafe
class MutablePoint {
public int x;
public int y;
public MutablePoint() {
x = 0;
y = 0;
}
public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}
}
上例中,可以并发的设置车辆位置和获取车辆当前位置,虽然MutablePoint
不是线程安全的,但MonitorVehicleTracker
是线程安全的,它所包含的Map对象和Point对象都未发布.当需要获取车辆位置时,通过MutablePoint
拷贝构造函数或者deepCopy
方法来复制正确的值,从而生成一个新的Map对象.
这种方式时通过在返回客户代码之前复制可变数据来维持线程安全的,但如果数据量特别多的话,拷贝就会带来性能问题.
3.线程安全性的委托
还是上面的例子,上面我们将车辆位置保存到一个Map中,因此需要先实现一个线程安全的Map类-ConcurrentHashMap.我们还可以用一个不变的Point类来代替MutablePoint以保存位置.
@ThreadSafe
public class DelegatingVehicleTracker {
private final ConcurrentHashMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> locations) {
this.locations = new ConcurrentHashMap<>(locations);
this.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);
}
}
}
@NotThreadSafe
class Point {
public final int x;
public final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
此例中将线程安全委托给了ConcurrentHashMap,虽然没有使用同步,但仍然是线程安全的类.所有对状态的访问都由ConcurrentHashMap来管理,而且Map所有的键和值都是不可变的.
3.1 独立的状态变量
可以将线程安全性委托给多个变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件.
public class VisualComponent {
//鼠标监听器和键盘监听器之间不存在任何关联
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
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);
}
}
VisualComponent
中,每个链表都是线程安全的,由于各个状态之间不存在耦合关系,因此VisualComponent
可以将它的线程安全性委托给keyListeners
和mouseListeners
等对象.
3.2 当委托失效时
如果多个变量之前存在联系(不变性条件),那么线程安全性就不能委托给变量.
@NotThreadSafe
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 not set lower to " + i + "> upper");
}
lower.set(i);
}
public void setUpper(int i) {
//注意:不安全的"先检查后执行"
if (i < lower.get()) {
throw new IllegalArgumentException("can not set upper to " + i + "< lower");
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
由于lower
和upper
之间不是彼此独立的,因此NumberRange
不是线程安全的.
可以采用加锁机制,使用一个锁来保护lower和upper,同时还需避免lower和upper发布.
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量.
3.3 发布底层的状态变量
如果想要发布一个底层状态变量,需要先确定它的不变性条件
上面的Counter
中的value
,逻辑上确定了value
是正整数,并且是递增的,如果发布了value
值,那么很可能客户代码会修改value
为一个无效值.所以不能发布value
.
VisualComponent
中的keyListeners
和mouseListeners
等变量就是线程安全的,因为类没有对其合法状态施加任何约束.
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量.
示例:
@ThreadSafe
public class PublishingVehicleTracker {
private final ConcurrentHashMap<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(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.replace(id, new SafePoint(x, y)) == null) {
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
locations.get(id).set(x,y);
}
}
@ThreadSafe
class SafePoint {
@GuardedBy("this")
public int x;
@GuardedBy("this")
public int 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;
}
/**
* get()方法返回的是x,y的一个拷贝数组
* 如果分别返回x,y,那么并发环境可能导致获取的间隙有线程修改了x或y,从而获取到一个从未有过的坐标
*/
public synchronized int[] get() {
return new int[]{x, y};
}
public synchronized void set(int x,int y) {
this.x = x;
this.y = y;
}
}
通过getLocation();
方法获取的SafePoint
是一个可变对象,可以调用其set(int x,int y)
方法修改其坐标.
PublishingVehicleTracker
是线程安全的,也成功的发布了SafePoint
,但如果他在车辆位置的有效值施加了任何约束(例如x坐标不能大于y坐标),那么就不再是线程安全的.
如果需要对车辆位置的变化进行判断或者当位置变化时执行一些操作,那么PublishingVehicleTracker
中采用的方法并不合适.
4.在现有的线程安全类中添加功能
如果现有的线程安全类不能满足所有需求,此时就需要在不破坏线程安全性的情况下添加一个新的操作.
要添加一个新的原子操作:
- 直接修改原始类,需要理解代码中的同步策略,或者直接新增一个方法.如果实现同步策略的代码在一个源文件中,这样就更加容易维护.但你通常无法修改一个类的源代码.
- 拓展这个类,利用继承,添加一个新的方法,但这种方法的同步机制异常脆弱.如果底层代码修改了同步策略,那么子类就会被破坏
4.1 客户端加锁机制
对于由Collections.synchronizedList
封装的ArrayList
,以上两种方法都行不通,因为客户代码并不知道在同步封装器工厂方法中返回的List对象类型.
第三种方式是:拓展类的功能,但并不是拓展类本身,而是将拓展代码放入一个"辅助类"中.
先看也错误示例:
@NotThreadSafe
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
上面示例中,list使用Collections.synchronizedList
保证同步,而putIfAbsent
也用了synchronized
保证同步,但是这两个锁并不是相同的锁,所以ListHelper
并不是线程安全的.
想要实现线程安全,就必须使用同一个锁:
@ThreadSafe
public class ListHelper<E> {
public final List<E> list = Collections.synchronizedList(new ArrayList<>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
}
}
通过这种方式加锁更加脆弱,使用时要更加小心.
4.2 组合
当为现有类添加一个原子操作时,有个更号的方法:组合
public class ImprovedList<T> implements List<T> {
private final List<T> list;
//传给构造函数后,就不会再使用这个list对象
public ImprovedList(List<T> list) {
this.list = list;
}
public synchronized boolean putIfAbsent(T x) {
boolean absent = !list.contains(x);
if (absent) {
list.add(x);
}
return absent;
}
...
}
ImprovedList
假设把某个链表对象传给构造函数以后,客户代码不会再直接使用这个对象,而只能通过ImprovedList
来访问它.
ImprovedList
不关心底层List是否线程安全,ImprovedList
通过自身的内置锁增加了一层额外的加锁.