首页 > 代码库 > Java 并发编程(四)同步容器类
Java 并发编程(四)同步容器类
同步容器类
Java 中的同步容器类包括 Vector 和 Hashtable ,二者是早起 JDK 的一部分,此外还包括在 JDK1.2 中添加的一些功能相似的类,这些同步的封装类是由 Collections.synchronizedXxx 等工厂方法创建的的。这些类实现线程安全的方法都是一样的:将他们封装起来,并对每个公有方法都进行同步,使得每次都只有一个线程能访问容器的状态。
同步容器类存在的问题
同步容器类都是线程安全的,但是某些情况下需要使用额外的客户端加锁来保护复合操作。
1、迭代操作。反复访问元素,直到遍历完 容器中所有的元素。
2、跳转操作。根据指定顺序,找到当前元素的下一个元素。
3、条件运算。例如“若没有则添加”的操作。
例如,对于容器遍历操作。这些符合操作在没有客户端加锁的情况下仍然是线程安全的,但是其他线程并发的修改容器时,它们可能会出现意料之外的行为。比如抛出 ArrayIndexOutOfBoundsException 。
由于同步容器类要遵守同步策略,即支持客户端加锁,因此可以创建一些新的操作,只要我们知道应该使用哪一个锁,那么这些新操作就与容器的其他操作一样都是原子操作。同步容器类根据自身的锁来保护它的每个方法。
for(int i =0;i<vector.size();i++){ doSomething(vector.get(i)); }
这种迭代操作存在竞态条件,即它的正确性依赖于多个线程执行的顺序。如果在 size 和 get 之间有别的线程修改了 Vector 时,则可能导致错误。如果在对 Vector 进行迭代时,另一个线程删除了一个元素,并且这两个操作交替执行,那么这种迭代方法将抛出 ArrayIndexOutOfBoundsException 异常。
虽然这个迭代操作可能会抛出异常,但是并不意味着 Vector 就不是线程安全的。Vector 的状态仍然是有效的,而抛出的异常也与其规范保持一致。然后,像在读取最后一个元素或者迭代这样的简单操作中抛出异常显然不是人们所期望的。
我们可以通过在客户端加锁来解决不可靠迭代的问题,但要牺牲一些伸缩性。通过在迭代期间持有 Vector 的锁,可以防止其他线程在迭代器件修改 Vector ,如下面程序所示,这样会导致其他线程在迭代期间无法访问他,因此降低了并发性。
synchronized (vector) { for (int i = 0; i < vector.size(); i++) { doSomething(vector.get(i)); } }
迭代器与 ConcurrentModificationException
无论是直接迭代还是在 for-each 循环的语法中对容器迭代,使用的方法都是 Iterator。然而,如果有其他线程并发的修改容器,那么即使是使用迭代器也无法避免在迭代器件对容器加锁。在设计同步容器类的迭代器是,并没有考虑到并发修改的问题,并且他们表现出的行为是“及时失败” (fail-fast) 的。这意味着,但它们发现容器在迭代过程中被修改时,就会抛出 ConcurrentModificationException 异常。
这种“及时失败”的迭代器并不是一种完备的处理机制,而只是捕获这种错误并发出预警。他们采用的实现方式是,将计数器的变化与容器关联起来:如果在迭代器件计数器被修改,那么 hasNext 或 next 将抛出 ConcurrentModificationException 。
迭代器件加锁会带来一系列的问题
如果容器规模很大, 那么后续线程将会等待较长的时间。调用 doSomething 时持有锁,可能会产生死锁。长时间对容器加锁会降低程序的可伸缩性。持有锁的时间过长,竞争就可能越激烈,如果有多个线程在等待,那么将极大降低吞吐量和CPU的利用率。
如果不希望加锁,那么一种替代方法便是”克隆"容器,在副本上进行迭代。由于副本被封闭在线程内,因此其它线程不会在迭代器件对其进行操作,这样就避免了抛出 ConcurrentModificationException 的以藏。但是问题在于克隆存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及响应时间和吞吐量等方面的需求。
隐藏迭代器
某些情况下,迭代器的使用并没有那么明显。看下面这个例子。
public class IteratorTest{ private final static Set<Integer> set = new HashSet<Integer>(); public static void print() { System.out.print(set); } public synchronized void add(Integer i){ set.add(i); } }
print 方法可能会抛出 ConcurrentModificationException 的异常。这是因为编译器将字符串的连接操作转换为调用 StringBuilder.append(Object),而这个方法又会调用容器的 toString() 方法,标准容器的 toString 方法将迭代容器,并在每个元素上调用 toString 来生成容器内容的格式化表示。
当然,真正的问题是由于 IteratorTest 并不是线程安全的。在使用println 中的 set 之前,必须首先获取 IteratorTest 的锁,但在调试代码和日志代码中通常会忽视这个要求。
相应的,容器的 hashCode 和 equals 等方法也会间接地执行迭代操作,当容器作为零一个容器的元素或键值时,就会出现这种情况。同样,containsAll、removeAll 和retainAll 等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接地迭代器都可能抛出 ConcurrentModificationException 。
Java 并发编程(四)同步容器类