java并发编程实践 4.3 委托线程安全

缘起

《java并发编程实践》 4.3节

分析

举一个委托线程安全的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ThreadSafe
public class CountingFactorizer implement Servlet {
private final AtomicLong count = new AtomicLong(0); // 记录本servlet处理请求的次数
// 注意,这里将count设定为final的. 否则,CountingFactorizer一旦将count移情别恋的话,我们还必须要保证这种修改对使用本CountingFactorizer的线程可见. 所以尽可能的使用final
public long getCount() {
return count.get();
}

public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}

上述自定义Servlet——CountingFactorizer是线程安全的. 因为他的状态count本身是一个AtomicLong, 是线程安全的. 所以我们说CountingFactorizer将线程安全性委托给了AtomicLong.

下面我们来看看一个更加真实的委托线程安全的例子

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
35
36
37
@Immutable
public class Point {
public final int x,y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ThreadSafe
public class DelegateVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;

public DelegateVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<>(points);
this.unmodifiableMap = Collections.unmodifiableMap(points);
}

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 id: " + id);
}
}
}

注意,上面的代码,DelegateVehicleTracker将线程安全性委托给了ConcurrentHashMap.

注意,DelegateVehicleTracker已经和上一节的MonitorVehicleTracker的行为有所不同了. 这里getLocations方法返回的是一个不可变(即客户端线程A调用getLocations方法拿到返回的Map之后,不能直接对此map进行增删改的操作,会直接抛异常的)的视图. 但是这个视图又是实时的. 因为如果其他线程调用setLocation方法替换了底层的locations属性中的键值对的话,则此时之前线程A调用getLocations方法得到的视图再次遍历的话,会发生变化的(类似于发生了不可重复度RR).

而MonitorVehicleTracker中的getLocations得到的Map,是不会随着B线程调用setLocation方法改变locations结构而变化的. 因为MonitorVehicleTracker的getLocations方法调用的是locations属性的深复制.

实时性还是一致性,正如上一节所说,完全取决于你的需求. 没有绝对的好坏之分.

如果你希望拿到一个非实时的视图的话,但是又不希望像MonitorVehicleTracker那样通过深复制耗时实现. 则可以像下面实现

1
2
3
public Map<String, Point> getLocations() {
return Collections.unmodifiableMap(new HashMap<>(locations));
}

这是因为HashMap的构造器——我们重新将locations中的键值对塞进了一个新的map(new HashMap<>(locations))中. 所以B线程通过setLocations方法修改原先locations中的键值对并不会影响这个新的map

至此,我们上面的CountingFactorizer也好,DelegateVehicleTracker也好,都只是将自己的线程安全性委托给了一个状态变量. 那是不是能将一个类的线程安全性委托给多个状态变量呢? 这取决于这些变量之间是否有联合的不变约束.

例如下面的代码

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
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

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);
}

}

因为 keyListeners 和 mouseListeners是独立的,并且都是线程安全的, 所以VisualComponent可以将

自己的线程安全性委托给他俩. 即

1
如果一个类由多个彼此独立的线程安全的状态变量组成,则可以将整个类的线程安全性委托给这些状态变量。

否则的话,是不行的.

例如下面的例子

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
import java.util.concurrent.atomic.AtomicInteger;

public class NumRange { // 维护 [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("You can not do this~");
}
lower.set(i);
}

public void setUpper(int i) {
if (i < lower.get()) {
throw new IllegalArgumentException("You can not do this~");
}
upper.set(i);
}

public boolean isInRange(int i) {
return i >= lower.get() && i <= upper.get();
}

}

这是因为虽然lower和upper都是线程安全的,但是他俩之间有不变约束需要遵守,就是 lower<=upper, 所以在进行操作的时候需要加锁. 如果不加锁的话,该约束条件未必能一直满足(所以不仅要加锁,而且不能发布upper和lower). 举个很简单的例子,就是在setLower设置lower,先检查upper,此时是合法的,但是设置lower的同时,又有一根线程设置了upper。所以就算是所有状态都是线程安全的,但是他们之间有不变约束,所以如果不加锁的话,整个类也未必是线程安全的. 这一点很类似于3.1的最后讲到的volatile使用法则——volatile不能用于和其他变量存在不变约束的变量.

最后一个问题——什么时候可以发布这些线程安全的状态呢(上面的例子中的count、locations等等)?

我们有以下原则

1
2
3
如果一个状态是线程安全的,而且没有和任何不变约束有瓜葛,并且没有任何限制他的状态空间(例如,其实
只有[0,100]才是合法的,但是如果你将它发布,然后客户端代码将其改成1000,就不好了),那么该状态就
是可以发布的.

举个例子,上面的 keyListeners和mouseListeners这两个状态就是可以发布的.