4.1 设计线程安全的类
在设计线程安全的类的过程中,需要包含以下的三个基本的要素:
- 找出构成对象状态的所有变量
- 找出约束变量的不可变性
- 建立对象状态的并发访问管理
public final class Counter{
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long setValue(){
if (value == long.MAX_VALUE)
throw new IllegalStateException("Counter overflow");
return ++value;
}
}
如上,我们构造了一个线程安全的类,我们来找出它所满足的三个条件:
- 变量:value变量
- 约束:改变value前,判断value是否达到最大值```if (value == long.MAX_VALUE)
- 并发管理:通过synchronized关键字
##### 4.1.1 收集同步需求
> 如果不了解对象的不变性条件与后验条件,那么就不能保证线程的安全性。要满足在状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性和封装性。
我们先来理解这两个名词:
- 不可性条件:在许多类中都定义了一些不可变条件,用于判断状态是否有效。比如,在上面的例子中,value的值必须满足在```Long.MIN_VALUE```和```Long.MAX_VALUE```之间。
- 后验条件:比如上面counter中当前状态为17,那么下一个有序状态只能为18。当下一个状态需要依赖上一个状态时,这个操作就必须是一个复合操作。
##### 4.1.2 依赖状态的操作
> 如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为“依赖状态的操作”。比如,在不能从空队列中删除元素,因此,在删除元素之前需要检查队列是否为空。
##### 4.1.3 状态的所有权
>状态变量的所有者将决定采用何种加锁协议来维持状态的完整性。所有权意味着控制权。但,如果发布了某个可变对象的引用,那么就不再拥有独立的控制权,最多是“共享控制权”。
#### 4.2 实例封闭
> 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保在访问数据时总能持有正确的锁。
注意:被封闭的对象一定不能超过它们既定的作用域。对象可以封闭在类的一个实例(比如私有成员)中,或者封闭在某个作用域内(比如一个局部变量),再或者封闭在线程内。
public class PersonSet{
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
public synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}
如上,我们将PersonSet进行了实例封闭,将数据mySet设置为私有成员,并通过方法来对数据进行访问。
##### 4.2.1 Java监视器模式
> 对于任何一种锁对象,只要自始自终都使用该锁对象,都可以用来保护对象的状态。
public class privateLock{
private final Object myLock = new Object();
Widget widget;
void someMethod(){
synchronized(myLock){
//访问或修改Widget的状态
}
}
}
如上为通过一个私有锁来保护状态
###### 示例:基于监视器模式的车辆追踪
我们要求视图线程和更新线程并发地访问数据模型,因此该模型必须是线程安全的。
- 记录轨迹的点:
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 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 void setLocations(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 Map<String,MutablePoint> deepCopy(Map<String, MutablePoint> map){
Map<String,MutablePoint> result = new HashMap<>();
for (String id : map.keySet())
result.put(id,new MutablePoint(map.get(id)));
return Collections.unmodifiableMap(result);
}
}
- 模拟更新和访问数据:
public static void main(String[] args){
//初始化数据
Map<String,MutablePoint> map = new HashMap<>();
map.put("test1",new MutablePoint());
map.put("test2",new MutablePoint());
map.put("test3",new MutablePoint());
MonitorVehicleTracker tracker = new MonitorVehicleTracker(map);
//设置更新线程,每隔10s更新一次数据
Thread set_thread = new Thread(){
@Override
public void run() {
while (true){
Map<String,MutablePoint> locations = tracker.getLocations();
for (String key : locations.keySet()){
MutablePoint p = locations.get(key);
int dx = (int) (Math.random() * 10);
int dy = (int) (Math.random() * 10);
tracker.setLocations(key, p.x+dx, p.y+dy);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//设置视图线程,每隔10s获取一次数据,并显示
Thread get_thread = new Thread(){
@Override
public void run() {
while(true){
Map<String,MutablePoint> locations = tracker.getLocations();
for (String key : locations.keySet()){
MutablePoint p = locations.get(key);
System.out.println("key: " + key + " value: (" + p.x
+ ","+p.y + ")");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
set_thread.start();
get_thread.start();
}
- 总结:对于上面的模式,类MutablePoint不是线程安全的,但追踪器类是线程安全的。因为它所包含的map对象和可变的Point对象都未曾发布过。当需要返回车辆的位置时,通过MutablePoint拷贝构造函数来复制正确的值,从而生成一个新的对象。
- 评价:优点是location集合上内部的数据是一致的,缺点是每次调用getLocation都需要复制数据,这将影响到性能。
#### 4.3 线程安全性的委托
public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
public long getCount() {return count.get();}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factors(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
> 如上,我们将CountingFactorizer类的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是安全的,是因为AtomicLong是安全的。
###### 4.3.1 示例:基于委托的车辆追踪器
- 我们用不可变的Point类代替MutablePoint类:
public class Point{
public final int x,y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
}
这样能保证在返回location时,不需要复制。因为不可变的值是可以被自由地共享。
- 将线程安全委托给ConcurrentHashMap
public class DelegatingVehicleTracker{
private final ConcurrentHashMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String,Point> map){
locations = new ConcurrentHashMap<String,Point>(map);
unmodifiableMap = Collections.unmodifiableMap(map);
}
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);
}
}
我们可以看到将线程安全性交给了安全容器ConcurrentHashMap,从而免去了对方法的加锁。
##### 4.3.2 独立的状态变量
> 我们可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。
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);
}
}
如上,我们将键盘和鼠标监听器列表都委托给CopyOnWriteArrayList,因为两者之间是相互独立的,因此不会增加不变性条件。
##### 4.3.3 当委托失效时
> 如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态,那么可以将线程安全性委托给底层的状态变量。
public class NumberRange{
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 lower 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");
lower.set(i);
}
public boolean isInRange(int i){
return (i >= lower.get() && i <= upper.get());
}
}
如上,虽然AtomicInteger是线程安全的,但经过组合得到的类却不安全。由于状态变量lower和upper并不是彼此独立的,因此NumberRange不能将线程安全性委托给它的安全状态变量。
如果某个类含有复合操作,仅靠委托并不足以实现线程安全性时,需要提供自己的加锁机制以保证这些复合操作都是原子性。
##### 4.3.4 发布底部的状态变量
> 如果一个这个变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
##### 4.3.5 发布状态的车辆追踪器
- 定义一个安全且可变的Point类:
public class SafePoint{
private int x,y;
private SafePoint(int[] a) {this(a[0],a[1]);}
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;
}
}
注意:我们这里将x和y绑定在一起,因为如果分别为x和y提供get方法,那么在获得这两个不同坐标的操作之间,x和y的值发生变化,从而导致调用者看到不一致的值。
- 安全发布底层状态的车辆追踪器:
public class PublishingVehicleTracker{
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String,SafePoint> map){
locations = new ConcurrentHashMap<String,SafePoint>(map);
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.containsKey(id))
throw new IllegalArgumentException(
"Invalid vehicle name: " + id);
locations.get(id).set(x,y);
}
}
注意:PublishingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,只是Map的元素是线程安全且可变的Point。getLocation方法返回底层Map对象的一个不可变副本。调用者不能增加或删除车辆,但却可以通过修改返回Map中的SafePoint值来改变车辆的位置。
#### 4.4 在现有的线程安全类中添加功能
> 假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加”的操作。
- 对原有的类进行扩展:
public class BetterVector<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
评价:如果底层的类改变了同步策略并选择了不同的锁来保护它的状态量,那么子类会被破坏。因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。
- 扩展类的功能:
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所用的锁和ListHelper所用的锁不是同一个锁。这意味着putIfAbsent相对于list的其他操作来说不是原子的。
- 客户端加锁:对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。
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;
}
}
}
如此,就能保证ListHelper所用的锁和list所用的锁是一致的。
##### 4.4.2 组合:实现接口的方式
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 absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
ImprovedList通过自身的内置锁增加了一层额外的加锁来实现线程的安全性。