摘要
同步容器类 : Vector , Hashtable ,以及Collections.synchronizedXxx等创建的类;
特征:将自身状态封装起来,并对每个共有方法都进行同步,使得每次只有一个线程能访问容器的状态;
关键字
复合操作,迭代器,隐式迭代操作,ConcurrentModificationException
复合操作上的并发问题
程序清单 1 Vector 上可能导致混乱结果的复合操作
public static Object getLast(Vector list){
int lastIndex = last.size() - 1;
return list.get(lasteIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
以上两个方法在同一个线程内使用的时候,不会有问题,但是如果分别在两个线程内执行,当getLast方法执行到list.get...时,假设从当前Vector获取的容器大小为5,这时CPU将时间片分配给第二个线程,第二个线程执行deleteLast方法,并完成从list中移除一个元素,那么当前容器的大小则为 4 ,紧接着CPU将时间片重新分配给第一个线程继续执行getLast方法中的list.get(lasteIndex) , list.get(4)就会报ArrayIndexOutOfBoundsException异常。
如何解决这类问题?
程序清单 2 复合操作加锁保证原子性
public static Object getLast(Vector list){
synchronized(list){
int lastIndex = last.size() - 1;
return list.get(lasteIndex);
}
}
public static void deleteLast(Vector list){
synchronized(list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
容器上的复合操作包含:迭代,跳转,以及条件运算,例如addIfAbsent, 这些操作在其他线程并发修改容器时,可能会表现出意料之外的行为。虽然容器在复合操作下抛出异常,但同步容器依然是线程安全的,例如Vector,其状态仍然有效,而抛出的异常也与其规范保持一致。但是抛出异常显然不是用户所期待的。另外同步容器加锁保证复合操作的原子性降低了并发性,假设在迭代过程中,为了防止其他线程对容器进行修改而对容器加锁,那么如果容器内元素的数量比较大,会导致迭代过程中其他线程无法对其进行访问。
迭代器与ConcurrentModificationException
首先看一个与线程安全/同步无关的问题示例如下:
程序清单 3 迭代过程中修改容器
public class IteratorExceptionTest {
public static void main(String[] args) {
List<String> temp = new ArrayList<>();
temp.addAll(Arrays.asList("1", "2", "3"));
for (String s : temp) {
temp.remove(s);
}
}
}
以上源码结果输出:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.learn.concurrency.IteratorExceptionTest.main(IteratorExceptionTest.java:11)
将以上执行结果称为“及时失败”(fail-fast) 处理机制,将容器的变化与计数器联系起来,如果在迭代期间计数器被修改,那么hasNext或者next将抛出ConcurrentModificationException。
程序清单 4 使用迭代器修改容器
public class IteratorExceptionTest {
public static void main(String[] args) {
List<String> temp = new ArrayList<>();
temp.addAll(Arrays.asList("1", "2", "3"));
Iterator<String> iterator = temp.iterator();
while(iterator.hasNext()){
String next = iterator.next();
if("2".equals(next)){
iterator.remove();
}
}
System.out.println(temp);
}
}
以上源码结果输出:
[1, 3]
以上结果表明,以迭代器的方法对容器进行修改,将不会抛出异常。其实内部也正是因为迭代器在对容器进行修改时,会同步修改容器的计数器,所以不会抛出异常。
那我们再回头想一下并发处理下,容器的执行效果,其实很自然的会发生一种情况,当容器在迭代过程中,其计数器达到N时,另一个线程通过对容器进行修改,从而将计数器值修改为M,那容器再继续迭代下去,就肯定会抛出异常。
其实这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示器。
如何解决此类问题?
其实这类问题与复合操作中的并发问题类似,都可以采用在迭代过程持有容器的锁来解决,但是缺点是一致的,如果容器的规模很大,或者在每个元素上执行操作的时间很长,其他线程将长时间等待,持有锁的时间越长,那么在锁上的竞争就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和CPU的利用率。
那有其他方案吗?可以借鉴其他容器的想法,如"克隆容器" , 在副本上进行迭代,由于副本只会在线程内部进行操作,则不会被其他线程修改,这样就避免抛出异常,但是克隆容器存在显著的性能开销,这种方式的好坏取决于:容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。
隐式迭代器
程序清单 5 隐式使用迭代器
public class HiddenIterator {
public static void main(String[] args) {
List<Integer> integers = Collections.synchronizedList(new ArrayList<>(Arrays.asList(1, 2, 3, 4)));
System.out.println("list : " + integers);
}
}
以上程序输出结果可能会抛出ConcurrentModificationException异常,以下为这两句代码的反汇编后的代码:
public class com.learn.concurrency.HiddenIterator {
public com.learn.concurrency.HiddenIterator();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: iconst_4
5: anewarray #3 // class java/lang/Integer
8: dup
9: iconst_0
10: iconst_1
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: aastore
15: dup
16: iconst_1
17: iconst_2
18: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
21: aastore
22: dup
23: iconst_2
24: iconst_3
25: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
28: aastore
29: dup
30: iconst_3
31: iconst_4
32: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
35: aastore
36: invokestatic #5 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
39: invokespecial #6 // Method java/util/ArrayList."<init>":(Ljava/util/Collection;)V
42: invokestatic #7 // Method java/util/Collections.synchronizedList:(Ljava/util/List;)Ljava/util/List;
45: astore_1
46: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
49: new #9 // class java/lang/StringBuilder
52: dup
53: invokespecial #10 // Method java/lang/StringBuilder."<init>":()V
56: ldc #11 // String list :
58: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
61: aload_1
62: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
65: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
68: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
71: return
}
从code = 49行开始看,其内部实例化一个StringBuilder,可见其要完成的操作是:"list : " + integers
,在62行对integers进行append的时候,采用了toString方法,标准容器的toString方法将迭代容器,并在每个元素上调用toString来生成容器内容的格式化标识。很明显,如果在容器迭代过程中,被其他线程所修改,就会抛出异常。
程序清单 6 AbstractCollection中的toString方法内部实现
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。
容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值是,就会出现这种情况。同样,containsAll, removeAll和retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都有可能抛出ConcurrentModificationException。
小结
同步容器只是对其各个操作进行加锁处理,但是在复合操作下,仍然不能达到原子性,需要自己额外加锁保证线程的安全性。隐式的迭代操作主要做了一些复合操作,这些操作由于非原子性,所以在并发处理中,存在线程安全问题。