在本章中涉及的组合模式中,将会把一些现有的线程安全组件组合成更大的程序,从而实现线程安全。
设计线程安全的类 我们可以通过封装来确保程序是线程安全的。线程安全的类的设计过程:
找出构成对象状态的所有属性
找出约束这些属性的不变性条件
建立这些属性的并发访问策略(同步策略
)
比如我们想要设计出下面这样的线程安全的程序,需要什么样的过程?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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; } }
首先,一目了然的是:
设计步骤1 : 只有value属性
第三步的同步策略(Synchronized Policy)
定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作进行协同。为了规定同步策略,我们需要先找出对象的不变条件或后验条件等同步需求。
收集同步需求
不可变条件
: 用于来判断状态是否有效。设计步骤2.1 : value属性是long类型,所以他的状态必需是Long.MIN_VALUE
到Long.MAX_VALUE
,并且在目前的代码中value属性不能是负值。
后验条件
:来判断状态迁移是否是有效。
也就是说,当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作(读取-修改-写入)。
设计步骤2.2 : 如果Counter的当前值为17,那么下一个有效状态只能是18。必须是复合操作
先验条件
: 某些对象的方法中还包含一些基于状态的先验条件。比如不能从空队列中移除元素,这种就是依赖状态的操作 。
状态的所有权
: 一个对象的状态将是对象图中所有对象包含的域的一个子集。 比如一个HashMap中包含多个Entry,Entry中可能包含了内部对象。只要它封装了某个对象,那么他就有那个对象的所有权。而一旦把他发布出去那他就不再有独占的控制权,而是共享控制权
。容器类中的大多都是所有权分离
的形式。
实例封闭 实例封闭:把一个对象封装到另一个对象中,从而限制他的访问途径。配合加锁策略使用,使得对象本身不会逸出。换句话说就是,将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ThreadSafe public class PersonSet { @GuardedBy ("this" ) 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); } interface Person { } }
在上面的代码中唯一能访问mySet的代码路径是addPerson与containsPerson,在执行它们它们时都要获得PersonSet上的锁(this)。PersonSet的状态完全由它的内置锁保护,因此PersonSet是一个线程安全的类。
设计步骤3 :使用实例封闭的方式,限制外部对value的访问
Java监视器模式 JAVA每个对象(Object/class) 都关联一个监视器,更好的说法应该是每个对象(Object/class)都有一个监视器,对象可以有它自己的临界区,并且能够监视线程序列为了使线程协作。
`Java监视器模式`:一种编写代码的约定。把所有可变状态都封装起来,并由对象自己的 内置锁保护。Vector
和HashTable
都使用了这种同步策略。
1 2 3 4 5 6 7 8 9 10 11 public class PrivateLock { private final Object myLock = new Object(); @GuardedBy ("myLock" ) Widget widget; void someMethod () { synchronized (myLock) { } } }
私有的锁对象可以将锁封装起来,只让客户端通过公有的方法去获取他。
线程安全性的委托 在很多情况下,通过多个线程安全类组合而成的类是线程安全的。(虽然在某些情况下,这仅仅是一个好的开端)
在第二章中的CountingFactorizer类是通过一个AtomicLong来保护count属性,因此CountingFactorizer是线程安全的。换句话说就是CountingFactorizer将它的线程安全性委托 给AtomicLong来保证:因为AtomicLong是线程安全的,所以CountingFactorizer是线程安全的。
基于委托的车辆追踪器 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 @ThreadSafe public class DelegatingVehicleTracker { private final ConcurrentMap<String, Point> locations; private final Map<String, Point> unmodifiableMap; public DelegatingVehicleTracker (Map<String, Point> points) { locations = new ConcurrentHashMap<String, Point>(points); 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); } public Map<String, Point> getLocationsAsStatic () { return Collections.unmodifiableMap( new HashMap<String, Point>(locations)); } }
在上面的程序中,所有对状态的访问都由ConcurrentHashMap来管理,Map所有的键值对都是不可变的,如果线程A调用getLocations,而线程B随后修改了某些点的位置,那么在返回给线程A的Map中将反映出这些变化。
独立的状态变量 我们还可以将线程安全委托给多个状态变量,只要这些变量是彼此独立 的。在下面的代码中VisualComponent 可以将线程安全性委托给两个线程安全的监听器列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); } }
当委托失效时 但大多数组合对象都不会像VisualComponent这样简单,在它们的状态变量之间存在着某些不变性条件。
😧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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" ); upper.set(i); } public boolean isInRange (int i) { return (i >= lower.get() && i <= upper.get()); } }
上面的代码是线程不安全的,因为setLower和setUpper都是“先检查后执行”的操作,但它们没有使用足够的加锁机制来保证这些操作的原子性,可能出现(5,4)的取值范围。
虽然AtomicInteger是线程安全的,但由于lower和upper不是彼此独立,因此委托失效,需要另外实现加锁机制。volatile的变量规则也是如此。
发布底层的状态变量 如果一个状态变量是线程安全,并且没有任何不变性条件来约束它的值,在变量的操作上也存在任何不允许的状态转换,那么就可以安全地发布这个变量。
例如,发布VisualComponent中的mouseListener和keyListener等变量就是安全的。由于VisualComponent并没有在监听器链表的合法状态上施加任何约束,因此这些与可以声明为公有域或者发布,而不会破坏线程安全性。
在现有的线程安全类中添加功能 Java类库中包含许多有用的“基础模块”类,通常,我们应该优先选择重用这些现有的类而不是创建新的类。那么该在现有的线程安全类中添加功能呢?
策略1 : 修改原始的线程安全类,从而添加新功能。 但通常无法做到,因为你可能无法访问或者修改类的源代码。若要修改就必须理解原来的同步策略。
策略2 : 通过继承去扩展原始的类
对Vector进行了扩展(继承了Vector):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ThreadSafe public class BetterVector <E > extends Vector <E > { static final long serialVersionUID = -3963416950630760754L ; public synchronized boolean putIfAbsent (E x) { boolean absent = !contains(x); if (absent) add(x); return absent; } }
但是这种通过“继承”的办法更脆弱,因为当父类的同步策略发生变化时,子类可能无法再用正确的方式进行并发访问。
客户端加锁机制 策略3:客户端加锁机制—扩展类的功能,但并不是继承类本身,而是将扩展代码放入一个“辅助类”中。
😧
1 2 3 4 5 6 7 8 9 10 11 12 @NotThreadSafe class BadListHelper <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; } }
上面的代码是错误的,因为尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的。
😊 修改后的ListHelper,客户端加锁或外部加锁使用的都是同一个锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @ThreadSafe class GoodListHelper <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; } } }
这种客户端加锁
相比上面的继承策略更加脆弱,因为它将类C的加锁代码放到与C完全无关的其他类中,当在那些并不承诺遵守加锁策略的类上使用客户端加锁时,要特别小心。
组合 策略4 : 组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @ThreadSafe 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 contains = list.contains(x); if (contains) list.add(x); return !contains; } public synchronized void clear () { list.clear(); } }
通过自身的内置锁来保护List
,也就是通过实例封闭来封装现有的List。事实上这种策略是最健壮的,虽然可能会降低性能。
将同步策略文档化 在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
Reference Java并发编程实战
源代码
Java并发编程实战(学习笔记三 第四章 对象的组合)