java并发编程实践 4.2 实例限制

缘起

《java并发编程实践》 4.2节

分析

即使一个对象不是线程安全的(即不是3.5最后的 shared thread-safe),仍然有很多技术可以让它安全的用于多线程程序. 例如线程限制, 或者使用锁.

实例限制就是将一个对象A封装到另一个对象中去,那么所有对A访问的代码路径就是可以预知的了. 从而把对A的访问限制在方法上,更易保证线程在访问数据的时候获取到正确的锁. 分析线程安全性的时候,就不必检查完整的程序.

A一定不能逸出到它期望的范围之外(不能发布它,逸出即bug),可以将A限制在类的私有成员变量上(private, 如下面的例子)、本地变量(local)或者线程本地变量(ThreadLocal).

下面给出了一个例子

1
2
3
4
5
6
7
8
9
10
11
@ThreadSafe
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);
}
}

JDK的类库中有很多实例限制的实例. 例如很多类存在的唯一目的就是为了把非线程安全的类转换为线程安全的. 例如ArrayList和HashMap这样的容器都是非线程安全的, 但是JDK提供了诸如Collections.synchronizedList等等工具类方法将这些非线程安全容器转换通过包装模式转换为线程安全的容器. 举个例子就是

java.util.Collections.SynchronizedMap<K, V>

1
2
3
4
5
6
7
8
9
10
11
12
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;

private final Map<K,V> m; // Backing Map 被包装对象
final Object mutex; // Object on which to synchronize synchronized互斥锁
...
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
...
}

上面看到了synchronized关键字. 这个关键字在之前的章节(2.3)讲过. 叫做内部锁或者互斥锁,其实还有一个别称——监视器锁. 甚至synchronized关键字在字节码中就翻译成 monitorenter(进入同步块的字节码)和monitorexit(退出同步块的字节码).

下面的例子展示了使用锁的最佳实践

1
2
3
4
5
6
7
8
9
10
public class PrivateClock {
private final Object myLock = new Object();
Widget widget;

void method() {
synchronized(myLock) {
// 访问或修改widget的状态
}
}
}

注意,上面使用的是类内部的私有的锁(外部代码无法获取, 因为没有发布),而不是this这种公开的可被任何外部代码获取的锁. 使用内部私有的锁的好处是显然的——访问外部代码不正确的使用锁引起活跃度的问题. 使用私有锁使得检验程序是否正确的使用锁变得简单,因为能获取到锁的仅仅是内部的代码.

下面的例子对于我们CRUD程序员是由裨益的. 因为至少我平时不会这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@ThreadSafe
public class MonitorVehicleTracker {
private final Map<String, MutablePoint> locations;

public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
this.locations = deepCopy(locations); // 不会简单的把外部传入的map赋值给this.locations, 不然的话, 就发布了私有的locations. 而本例就是想做实例限制
}

public synchronized Map<String, MutablePoint> getLocations() { // 吐槽一下,没有使用私有锁,而是使用了公有的锁this
return deepCopy(locations); // 不会发布locations属性, 但是这将面对一个问题就是, 每次都要拷贝, 导致接口的响应时间较长.
}

public synchronized MutablePoint getLocation(String id) {
MutablePoint location = locations.get(id);
return location==null? null:new MutablePoint(location); // 不暴露内部私有的对象, 而是new一个返回
}

public synchronized setLocation(String id, int x, int y) {
MutablePoint location = locations.get(id);
if(location == null) {
throw new IllegalArgumentException("No Such ID: " + id);
}
location.x = x;
location.y = y;
}

private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> locations) {
Map<String, MutablePoint> result = new HashMap<>();
for(String id : locations.keySet()){
result.put(id, new MutablePoint(locations.get(id))); // "深拷贝"
}
return Collections.unmodifiableMap(result); // 返回不可变的视图
}
}

其中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /*
返回指定map一个只读的视图.只允许用户对其中的成员变量m进行只读操作,其他写的操作将抛出UnsupportedOperationException异常
*/
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) {
return new UnmodifiableMap<>(m);
}
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {
private final Map<? extends K, ? extends V> m;
...
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
...
}

其中MutablePoint的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NotThreadSafe
public class MutablePoint {
public int x,y;
public MutablePoint() {
this.x = 0;
this.y = 0;
}

public MutablePoint(MutablePoint p) {
this.x = p.x;
this.y = p.y;
}

get/set方法...
}

注意上面的deepCopy方法, 其实不仅仅是降低接口性能这么简单. 因为你返回了一个里面的元素都是new出来的MutablePoint(而不是原本的引用), 所以就算后续MonitorVehicleTracker中的locations属性发生变化, 返回的视图也不会相应变化. 也就是上面的getLocations方法返回的数据可能是过期的. 但是这并非没有任何好处, 因为这样的话,保证了返回的这个视图的某种一致性. 也就是实时性换一致性. 具体需要哪种,看你需求。