同步容器类和并发容器介绍。

1. 同步容器类

同步容器类包括 Vector 和 Hashtable,也包括在 JDK1.2 中添加的一些功能相似的类,这些同步的封装器类是由 Collections.synchronizedXxx 等工厂方法创建的。
这些类实现线程安全的方式是:将它们的状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态。

1.1 同步容器类的问题

同步容器类都是线程安全的,但在某些情况下可能要额外的客户端加锁来保护复合操作。

// 虽然多线程不会破坏 Vector,但是结果却可能不是调用者预期的
// 例如:交替调用 getLast 和 deleteLast 时抛出 ArrayIndexOutOfBoundsException
// A : size->10 ---- ---- ---- ----> get(9) ----> 出错
// B : size->10 ----> remove(9)

public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}

public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

只要我们知道应该使用哪一个锁,就可以实现复合操作原子性。

// 在加锁的 Vector 上的符合操作
public static Object getLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
}

public static void deleteLast(Vector list) {
    synchronized (list) {
        int lastIndex = list.size - 1;
        list.remove(lastIndex);
    }
}

加锁降低了并发性

1.2 迭代器与 ConcurrentModificationException

在设计同步容器类的迭代器时并没有考虑到并发修改的问题,它表现出的行为是“及时失败(fail-fast)”的。
这意味着,当发现容器在迭代过程中被修改时,会抛出 ConcurrentModificationException。

如果不希望在迭代其间对容器加锁,一种替代方法是“克隆”容器,并在副本上进行迭代。

1.3 隐藏迭代器

例:

...
private final Set<Integer> set = new HashSet<Integer>();
...
public void addTenThings() {
    ...
    // 编译器将字符串连接操作转换为 StringBuilder.append(object),而这个方法会调用容器的 toString 方法,
    // 标准容器的 toString 方法会遍历容器,并在每个元素上调用 toString 来生成容器内容的格式化表示
    System.out.println("DEBUG: added ten elements to " + set);
}

容器的 hashCode 和 equals 等方法也会间接执行遍历操作,containsAll, removeAll 和 retainAll等,以及把容器作为参数的构造函数,都会对容器进行迭代。 所有这些间接迭代都可能抛出 ConcurrentModificationException。

2. 并发容器

Java5.0 增加了 ConcurrentHashMap,以及 CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下代替同步的 List。

2.1 ConcurrentHashMap

使用更细粒度的加锁机制来实现更大程度的共享,称为分段锁(Lock Striping)
任意数量的读线程可并发访问 Map,一定数量的写线程可以并发访问。并发环境下更高吞吐量,单线程环境中损失非常小的性能。

ConcurrentHashMap 与其他并发容器的迭代器不会抛出 ConcurrentModificationException。 ConcurrentHashMap 返回的迭代器具有弱一致性(Weakly Consisten),而非“及时失败”。例如 size 和 isEmpty,只是返回一个估计值,因为在返回的过程中实际值已经变了。

2.2 CopyOnWriteArrayList

写入时复制(Copy-On-Write)容器的线程安全性在于,只要正确发布一个事实不可变的对象,那么在访问该对象时就不需要进一步同步。
在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。

每当修改容器时都会复制底层数组,这需要开销,特别是规模较大时。所以仅当迭代操作远远多于修改时,才应该使用“写入时复制”容器。 例如事件通知系统:在分发通知时需要迭代已注册监听器链表,并调用每一个监听器,在大多数情况下,注册和注销事件鉴定器的操作远少于接收事件通知的操作。