众所周知,HashMap 和 ArrayList 等常用的容器类并不是线程安全的,但在单线程模型下,他们有着很好的执行效率
早期,java 通过加锁的方式实现了两个线程安全的同步容器类:Vector 和 Hashtable
我们也可以使用 java 类库中提供的 Collections 类的 synchronizedXxxx 方法来创建非线程安全容器的同步容器类
如:
List widgetList = Collections.synchronizedList(new ArrayList());
同步容器类的问题
同步容器类是线程安全的,但是有些情况下需要客户端加锁来保护复合操作
常见的复合操作包括:迭代(遍历容器)
跳转(找到下一个元素)
条件运算
虽然同步容器类的所有操作都是线程安全的,但是当其他线程修改容器时,上面列出的复合操作就会表现出意料之外的行为
public static Object getLast(Vector list) {
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
上面的代码非常典型,如果虽然 size 操作和 get 操作都是线程安全的,但在他们之间,可能有另一个线程删除了 list 中的一个元素,那么,在最后就会抛出 ArrayIndexOutOfBoundsException 异常
所以这种情况下,客户端必须对整个表进行加锁,以保护复合操作
迭代器与 ConcurrentModificationException
无论是直接使用迭代器进行迭代操作还是使用 for-each 循环语法,他们都是使用 Iterator 对象来实现迭代操作的
如果有其他线程并发的修改容器,那么使用迭代器就必须在迭代期间对容器加锁,然而 jdk 的设计中并没有考虑到这个问题,在实际环境中,如果容器在迭代过程中被其他线程修改,那么就会抛出一个 ConcurrentModificationException 异常
解决这个问题,最直观的方法就是加锁,但是加锁意味着多个线程的竞争,可能会极大地降低吞吐量和 CPU 利用率,另一种替代的方法是克隆容器,在副本上进行迭代,这样就可以保证其他线程不会去对其进行修改,他的主要开销在于创建克隆容器的阶段