利用对象限制和委托构建线程安全的类(java并发编程实战第四章内容)

设计线程安全的类需要考虑:

1. 确定组成对象状态的变量.

2. 确定约束对象状态的不变式.

3. 建立并发访问对象状态的规则.

 

后置条件: 由于某些变量的取值是有限制范围的, 改变状态变量之后需要检查改变后的状态是否合法. 后置条件要求进行额外的同步. 比如一个计数器, 当前的值为17, 那么进行一次操作之后其取值只能是18, 其他值都是非法的.

前置条件: 需要满足一定的条件操作才能继续进行. 多线程环境下, 可以使用内置的wait和notify机制, 在条件满足时通知另一个线程继续往下执行. 也可以使用阻塞队列, 信号量等实现. 

 

对象限制

借助封装, 可以将对象限制在类范围内(作为类的私有成员), 方法内部(作为方法的局部变量), 或者线程范围内(只能在特定线程中访问该对象).

将对象限制在类范围内的例子:

Java代码  收藏代码
  1. public class PersonSet {   
  2.     private final Set<Person> mySet = new HashSet<Person>();   
  3.   
  4.     public synchronized void addPerson(Person p) {   
  5.         mySet.add(p);   
  6.     }   
  7.   
  8.     public synchronized boolean containsPerson(Person p) {   
  9.         return mySet.contains(p);   
  10.     }   
  11. }   

mySet对象不是线程安全的, 但是将mySet对象限制在PersonSet类中, 并通过synchronized同步所有访问mySet对象的方法, 而且mySet对象没有逃逸出PersonSet类范围, PersonSet就是一个线程安全的类.

由上面的例子可知, 受限的对象不能逃逸出所在的范围是非常重要的.

另一个例子是java.util.Collections类提供的一些静态方法, 比如Collections.synchronizedList(List<T> list)方法:

Java代码  收藏代码
  1. class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {  
  2.     static final long serialVersionUID = -7754090372962971524L;  
  3.   
  4.     final List<E> list;  
  5.   
  6.     SynchronizedList(List<E> list) {  
  7.         super(list);  
  8.         this.list = list;  
  9.     }  
  10.   
  11.     SynchronizedList(List<E> list, Object mutex) {  
  12.         super(list, mutex);  
  13.         this.list = list;  
  14.     }  
  15.   
  16.     public boolean equals(Object o) {  
  17.         synchronized (mutex) {  
  18.             return list.equals(o);  
  19.         }  
  20.     }  
  21.   
  22.     public int hashCode() {  
  23.         synchronized (mutex) {  
  24.             return list.hashCode();  
  25.         }  
  26.     }  
  27.   
  28.     public E get(int index) {  
  29.         synchronized (mutex) {  
  30.             return list.get(index);  
  31.         }  
  32.     }  
  33.     // ....  
  34. }  

可以看出SynchronizedList是对List的包装. SynchronizedList所有的方法都使用同一把锁mutex进行同步. 所以SynchronizedList是一个线程安全的类. 但是前提是被包装的list对象不能再直接使用, 必须通过synchronizedList(List<T> list)返回的对象进行访问(开发者必须遵循这样的约定, 否则线程安全就被破坏了), 也就是防止list对象逃逸出SynchronizedList类的范围.

 

委托线程安全. 先看一个例子:

Java代码  收藏代码
  1. public class DelegatingVehicleTracker {  
  2.     private final ConcurrentMap<String, Point> locations;   
  3.   
  4.     public DelegatingVehicleTracker(Map<String, Point> points) {   
  5.     // 构造map的concurrent视图  
  6.         locations = new ConcurrentHashMap<String, Point>(points);   
  7.     }   
  8.   
  9.     public Map<String, Point> getLocations() {   
  10.         // 返回map的unmodifiable视图  
  11.         return Collections.unmodifiableMap(locations);   
  12.     }   
  13.   
  14.     public Point getLocation(String id) {   
  15.         return locations.get(id);   
  16.     }   
  17.   
  18.     public void setLocation(String id, int x, int y) {   
  19.         if (locations.replace(id, new Point(x, y)) == null)   
  20.             throw new IllegalArgumentException(   
  21.                 "invalid vehicle name: " + id);   
  22.     }  
  23.   
  24.     /** 
  25.      * 这是一个不可变类 
  26.      */  
  27.     public class Point {   
  28.         public final int x, y;   
  29.         public Point(int x, int y) {   
  30.             this.x = x;   
  31.             this.y = y;   
  32.         }   
  33.     }  
  34. }  

DelegatingVehicleTracker只有一个状态变量: locations. locations是一个ConcurrentMap对象, 也就是说locations是线程安全的. 而且locations对象被限制在DelegatingVehicleTracker类范围内. 因此可以说, DelegatingVehicleTracker是一个线程安全的类因为locations是线程安全的. 换一种说法, DelegatingVehicleTracker将线程安全的责任委托给了locations对象. 

在构造函数里, 利用points的concurrent视图初始化locations. concurrent视图不同于synchronized视图, synchronized视图只是包装了一层同步, 方法的调用还有由原有的Map对象负责的, 因此JDK文档特别要求不能在外部继续调用原有Map的方法, 所以线程安全来源于约定, 如果开发者没有遵循约定, 线程安全就被破坏了. 但是concurrent视图并非如此, concurrent视图是对原有Map的浅拷贝, 也就是会遍历原有Map, 取出其中的每一个键值对, 然后将取出的键值对存入concurrent视图中. 因此concurrent视图仅和原有的Map共享了键和值的引用, 而由于Map的value(Point对象)是不可变的, 在外部直接调用原有的Map的任何方法, 都不会对concurrent视图造成影响.

我们可以总结出这样的规则: 如果类只包含一个状态变量, 那么类是否是线程安全的, 取决于该状态变量是否是线程安全的.

如果类中存在多个状态变量时, 委托规则是否仍然成立呢? 如下:

Java代码  收藏代码
  1. public class VisualComponent {   
  2.     private final List<KeyListener> keyListeners   
  3.         = new CopyOnWriteArrayList<KeyListener>();   
  4.     private final List<MouseListener> mouseListeners   
  5.         = new CopyOnWriteArrayList<MouseListener>();   
  6.   
  7.     public void addKeyListener(KeyListener listener) {   
  8.         keyListeners.add(listener);   
  9.     }   
  10.   
  11.     public void addMouseListener(MouseListener listener) {   
  12.         mouseListeners.add(listener);   
  13.     }   
  14.   
  15.     public void removeKeyListener(KeyListener listener) {   
  16.         keyListeners.remove(listener);   
  17.     }   
  18.   
  19.     public void removeMouseListener(MouseListener listener) {   
  20.         mouseListeners.remove(listener);   
  21.     }   
  22. }  

CopyOnWriteArrayList是ArrayList的线程安全的变体, keyListeners和mouseListeners是线程安全的对象, 而且keyListeners和mouseListeners是相互独立的, 所以VisualComponent也是线程安全的. 当状态变量之间不是相互独立的时候情况又如何呢? 如:

Java代码  收藏代码
  1. public class NumberRange {   
  2.     // INVARIANT: lower <= upper   
  3.     private final AtomicInteger lower = new AtomicInteger(0);   
  4.     private final AtomicInteger upper = new AtomicInteger(0);   
  5.   
  6.     public void setLower(int i) {   
  7.         // Warning -- unsafe check-then-act   
  8.         if (i > upper.get())   
  9.             throw new IllegalArgumentException(   
  10.                     "can't set lower to " + i + " > upper");   
  11.         lower.set(i);   
  12.     }   
  13.   
  14.     public void setUpper(int i) {   
  15.         // Warning -- unsafe check-then-act   
  16.         if (i < lower.get())   
  17.             throw new IllegalArgumentException(   
  18.                     "can't set upper to " + i + " < lower");   
  19.         upper.set(i);   
  20.     }   
  21.   
  22.     public boolean isInRange(int i) {   
  23.         return (i >= lower.get() && i <= upper.get());   
  24.     }   
  25. }   

NumberRange类的状态变量lower和upper都是线程安全的对象, 但是lower和upper不是相互独立的, 两者需要满足不变式约束: lower <= upper. 假设lower和upper分别是0和10, 此时2个不同的线程同时调用setLower(5)和setUpper(4), 在某些不幸的时间点这2个方法的前置条件检查都能通过, 这就造成不变式约束不再成立. 因此NumberRange类不是线程安全的.

总结: 如果类中的状态变量之间是相互独立的, 那么线程安全的责任就可以委托给状态变量: 类是否线程安全, 取决于所有状态变量是否线程安全.

阅读更多
个人分类: java多线程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭