第四章——对象组合

4.2 实例封闭

4.2.2 示例:车辆追踪

程序清单 4-4 给出了一个基于 Java 监视器模式实现的“车辆追踪器”,其中使用了程序清单 4-5 中的 MutablePoint 来表示车辆位置。显然,更新车辆位置和获取车辆位置将在不同线程中进行,因此它们的读 / 写之间需要同步。

// 程序清单 4-4
@ThreadSafe
public class MonitorVehicleTracker {
    @GuardBy("this")
    private final Map<String, MutablePoint> locations;

    public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
        this.locations = deepCopy(locations);
    }
    
    // 通过 deepCopy 方法来深拷贝 locations 对象,并返回
    public synchronized Map<String, MutablePoint> getLocations() {
        return deepCopy(locations);
    }
    
    // 通过 MutablePoint 的拷贝构造函数来返回一个新的 MutablePoint 对象
    public synchronized MutablePoint getLocation(String id) {
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }
    
    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 static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        Map<String, MutablePoint> result = new HashMap<>();
        for (String id : m.keySet()) {
            result.put(id, new MutablePoint(m.get(id)));
        }
        return Collections.unmodifiableMap(result);
    }
}

// 程序清单 4-5
// 这个类并不是线程安全的,x, y 应申明为 final 类型,就是线程安全的
@NotThreadSafe
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;
    }
}

上面的 MutablePoint 不是线程安全的,但追踪器类是线程安全的。它所包含的 Map 对象和可变的 Point 对象都未曾发布。

  • 当通过 getLocations 返回所有车辆位置时,通过 deepCopy 方法来复制正确的值,从而生成一个新的 Map 对象,并且该对象的值与原有 Map 对象中的 key 值和 value 值都相同。
  • 当通过 getLocation 返回特定车辆位置时,通过 MutablePoint 的拷贝构造函数来复制正确的值,返回一个新生成的 MutablePoint 对象。

程序清单 4-4 通过 synchronized 关键字来控制多线程的方法,通过复制可变的数据来维持线程安全性。通常情况下,这并不存在性能问题,但在车辆容器非常大的情况下将极大地降低性能①。此外,由于每次调用 getLocation 时就要复制数据,因此一种错误情况——线程 A 在获取车辆位置信息后,但是还未使用时,线程 B 更新了车辆位置,之后线程 A 使用车辆位置信息时却还是旧值。这种情况是好是坏,要取决于你的需求。如果在 location 集合上存在内部的一致性需求,那么这就是优点,在这种情况下返回一致的快照就非常重要。然而,如果调用者需要每辆车的最新信息,那么这就是缺点,因为这需要非常频繁地刷新快照。

① 由于 deepCopy 是从一个 synchronized 方法中调用的,因此在执行时间较长的复制操作中, tracker 的内置锁将一直被占有,当有大量车辆需要追踪时,会严重降低用户界面的响应灵敏度。

4.3 线程安全性的委托

在上面 4.2 节中,我们的跟踪器类是由多个非线程安全的类组合而成,因此 Java 监视器模式(使用 synchronized 关键字,持有的锁是类内置锁,也就是监视器)就显得非常有用。但是,如果类中的各个组件都已经是线程安全的,我是否需要再增加一个额外的线程安全层?答案是,视情况而定。

4.3.1 示例:基于委托的车辆追踪器

下面我们将改造 MonitorVehicleTracker 类,构造一个委托给线程安全类的车辆追踪器。首先我们用一个不可变的 Point 类来代替 MutablePoint 以保存位置:

// 程序清单 4-6
// 由于有了 final 关键字,因此这是一个线程安全的不可变类
@Immutable
public class Point {
    public final int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

其次,我们在跟踪器类中使用线程安全的 ConcurrentHashMap 来代替 Map 类。

// 程序清单 4-7
@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<>(points);  // 将传入进来的 Map 转换为线程安全的 ConcurrentHashMap
        unmodifiableMap = Collections.unmodifiableMap(locations);  // unmodifiableMap() 连浅拷贝都算不上,它只是 locations 引用的代理
    }

    public Map<String, Point> getLocations() {
        return unmodifiableMap; // 由于 unmodifiableMap 只是 locations 的代理,而 locations 又是 ConcurrentHashMap 类型(具有可见性),因此 locations 的任何变化,外界都能及时观察到
    }

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

DelegatingVehhicleTracker 中没有使用任何显式的同步,所有对状态的访问都由 ConcurrentHashMap 来管理,而 ConcurrentHashMap 具有可见性的保证,而且 Map 所有的键和值都是不可变的。但是上面代码如果使用最初的 MutablePoint 类而不是 Point 类,就会破坏封装性,因为 getLocations 会发布一个指向可变状态的引用,而这个引用不是线程安全的。
我们稍微改变了车辆追踪器类的行为。在使用监视器模式的车辆追踪器中返回的是车辆位置的快照,而在使用委托的车辆追踪器中返回的是一个不可修改带却实时的车辆位置视图。这意味着,如果线程 A 调用 getLocations,而线程 B 在随后修改了某些点的位置,那么在返回给线程 A 的 Map 中将反映这些变化。在 4.2 节中提到过,这可能是一种优点(更新的数据),也可能是一种缺点(可能导致不一致的车辆位置视图),具体情况取决于你的需求。

如果需要跟 4.2 节中行为一样,那么我们只需使 getLocations 方法返回一个视图的快照。要达到这个目的,那么 getLocations 可以返回对 locations 这个 Map 对象的一个浅拷贝(Shallow Copy,即生成一个新的 Map,但是里面的每个 Entry 都是拷贝自原来的 Map):

public Map<String, Point> getLocations() {
    return Collections.unmodifiableMap(new HashMap<>(locations));  // HashMap 的拷贝构造函数是一次浅拷贝
}

上面返回的 unmodifiableMap 实际上是 locations 的一个浅拷贝。这样就保证了 getLocations 方法返回的是车辆位置信息的快照,因为 setLocation 方法中使用 replace 方法更新 locations 中的 Entry,并不是直接更新的原 Entry 中的数据,而是新构造了一个 Entry 来替换 locations 中的原 Entry。被替换掉的原 Entry 并不会被 gc,因为它的引用还存在 getLocations 返回的快照中,因此快照中的数据不会受 setLocation 方法更新数据的影响。

4.4 在现有的线程安全类中添加功能

Java 类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类。但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏线程安全的情况下添加一个新的操作。

例如,假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加(Put-If-Absent)”的操作。同步的 List类已经实现了大部分的功能,我们可以根据它提供的 contains 方法和 add 方法来构造一个“若没有则添加”的操作。

“若没有则添加”的概念很简单,在向容器中添加元素前,首先检查该元素是否已经存在,如果存在就不再添加。由于这个类必须是线程安全的,因此就隐含地增加了另一个需求,即“若没有则添加”这个操作必须是原子操作。

要添加一个新的原子操作,最安全的方法时修改原始的类,但这通常无法做到,因为你可能无法访问或修改类的源代码。

另一种方法是扩展这个类。程序清单 4-13 中的 BetterVectorVector 进行了扩展,并添加了一个新方法 putIfAbsent。扩展 Vector 很简单,但并非所有的类都像 Vector 那样将状态向子类公开,因此也就不是和采用这种方法。

// 程序清单 4-13
@ThreadSafe
public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if (absent)
            add(x);
        return absent;
    }
}

“扩展”方法比直接将代码添加到类中更脆弱,因为现在的同步策略实现被分不到多个单独维护的源代码中。如果底层的类改变了同步策略并选择了不同的锁类保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁类控制对基类状态的并发访问。

4.4.1 客户端加锁机制

对于由 Collections.synchronizedList 封装的 ArrayList,这两种方法在原始类中添加一个方法或者对类进行扩展都行不通,因为客户代码并不知道在同步封装器工厂方法中返回的 List 对象的类型。第三种策略是扩展类的功能,但并不扩展类本身,而是将扩展代码放入一个“辅助类”中。

// 程序清单 4-14
@NotThreadSafe
public class ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<>());
    
    // 这里的 synchronized 关键字不起作用:在错误的锁上进行了同步。
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if (absent)
            list.add(x);
        return absent;
    }
}

上面程序清单 4-15 中,synchronized 关键字持有的是 ListHelper 的内置锁,而不是 list 相关操作的锁,这就意味着 putIfAbsent 相对于 List 的其他操作来说并不是原子的,因此 ListHelper 并不是线程安全的。

要想使这个方法能正确执行,必须使 List 在实现客户端加锁或外部加锁时使用同一个锁。

// 程序清单 4-15
@NotThreadSafe
public class ListHelper<E> {
    public List<E> list = Collections.synchronizedList(new ArrayList<>());

    public boolean putIfAbsent(E x) {
        // 使用 list 自己的锁
        synchronized (list) {
            boolean absent = !list.contains(x);
            if (absent)
                list.add(x);
            return absent;
        }
    }
}

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。

4.4.2 组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)。

// 程序清单 4-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 的其他方法
}

ImprovedList 通过自身的内置锁增加了一层额外的加锁。它并不关心底层的 List 是否是线程安全的,即使 List 不是线程安全的或者修改了它的加锁实现,ImprovedList 也会提供一致的加锁机制来实现线程安全性。
而且,额外的同步层导致的性能损失其实非常小,因为在底层 List 上的同步不存在竞争,所以速度很快。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容