重用能降低开发工作量、开发风险(因为现有的类都已经通过测试)以及维护成本。有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新的操作。
Reusing existing classes is often preferable to creating new ones: reuse can reduce development effort, development risk (because the existing components are already tested), and maintenance cost. Sometimes a thread-safe class that supports all of the operations we want already exists, but often the best we can find is a class that supports almost all the operations we want, and then we need to add a new operation to it without undermining its thread safety.
通常可以通过修改原始类、扩展类、客户端加锁和组合的方法来在现有的线程安全类中添加功能。
- 修改原始类
要添加一个新的原子操作,最安全的办法就是修改原始的类,但这通常无法做到,因为你可能无法访问或修改类的源代码。要想修改原始的类,就需要理解代码中的同步策略,这样增加的功能才能与原有的设计保持一致。如果直接将新方法添加到类中,那么意味着实现同步策略的所有代码仍然处于一个源代码文件中,从而更容易理解与维护。
- 扩展类本身
比如扩展Vector并增加一个"若没有则添加"方法
import java.util.Vector;
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent) {
add(x);
}
return absent;
}
}
BetterVector对Vector进行了扩展并添加一个新方法putIfAbsent。扩展Vector很简单,但并非所有的类都像Vector那样将状态向子类公开,因此也就不适合采用这种方法。
扩展方法比直接将代码添加到源代码更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏。
- 客户端加锁机制
对于通过Collections.synchronizedList封装的ArrayList,修改原始类和扩展类本身都行不通,因为客户端代码并不知道同步封装器工厂方法中返回的list的类型。
For an ArrayList wrapped with a Collections.synchronizedList wrapper, neither of these approaches—adding a method to the original class or extending the class—works because the client code does not even know the class of the List object returned from the synchronized wrapper factories.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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;
}
}
Why wouldn’t this work? After all, putIfAbsent is synchronized, right? The problem is that it synchronizes on the wrong lock. Whatever lock the List uses to guard its state, it sure isn’t the lock on the ListHelper. ListHelper provides only the illusion of synchronization; the various list operations, while all synchronized, use different locks, which means that putIfAbsent is not atomic relative to other operations on the List.
以上程序清单中包含了一个若没有则添加操作的辅助类,用于对线程安全的list进行操作,但其中的代码是错误的。虽然在对应的方法putIfAbsent已经声明为synchronized的。问题在于在错误的锁上进行了同步。无论list使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁。因为在执行putIfAbsent方法的时候,另一个线程完全由可能移除list中的元素(ListHelper并不能保证其他针对list操作的同步)。这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表
要想使这个方法能正确执行,必须使List在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁时指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。
Client-side locking entails guarding client code that uses some object X with the lock X uses to guard its own state.In order to use client-side locking, you must know what lock X uses.
采用客户端加锁的方式修改以上代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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;
}
}
}
通过添加一个原子操作来扩展类是脆弱的,因为它将类的加锁代码分布到多个类中。然而,客户端加锁却更加脆弱,因为它将类C的加锁代码放到了与C完全无关的其他类中。当在那些并不承诺遵循加锁策略的类上使用客户端加锁时,需要特别注意。
客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。
- 组合
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();
}
// ... similarly delegate other List methods
}
ImprovedList通过自身的内置锁增加了一层额外的锁。它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现。ImprovedList也会提供一致的加锁机制来实现线程安全性。虽然额外的同步层可能会导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,ImprovedList更为健壮。