迭代器访问Collection与fail-fast机制

迭代器访问Collection与fail-fast机制

一个bug

这个问题要从今天看的一个bug说起。这个bug的原因是我们的程序有一个api,输入一个json,对于json的每一个字段查询一个结果后放在这个json里面,遇到一些特殊的查询情况我们会在json里面加一个字段作为标签。因为对每个字段的查询要访问elasticsearch,我们大量使用了future做并行。在遍历json字段时,我们用了这个的写法for (Entry<String, Object> entry : json.entrySet()) {...}。并且我们只在添加字段时对json做了sync,没有对for循环做sync。

因为在里面我们有可能个json添加字段,应该每次都是非法的,抛出ConcurrentModificationException异常。但是我们使用了future,并且访问es是一个时间很长的操作。所以在业务压力不大的情况下基本上for循环跑完了es访问也不会返回结果,这使得绝大部分情况下for循环遍历时json并没有添加字段,成为了一个合法访问。

而最近我们新上线了可视化产品,会大量调用这个接口,增加了并发性。这使得for循环可能没执行完时就被系统切出去将cpu让给future,从而在for循环过程中就有可能出现完成的future线程修改json本身,最终导致ConcurrentModificationException的发送。修改方式很简单,对for循环做sync即可,让他在遍历完之前所有future线程都无法对json做修改即可。

Iterator与Fail-fast

对于collection的遍历,无论是直接使用iterator还是用for(:)的形式,本质都是调用iterator。对于一个遍历来说,最糟糕的情况就是一边你在遍历一个collection,另一边collection被修改了,这会导致一些不可预测的情况。虽然这个不可预测的情况不一定是错误的,但是我们还是希望避免这种情况的发生。因此,在出现这种情况时迭代器会抛出一个ConcurrentModificationException。

常见的抛出这个错误的情况是你在遍历一个collection时,另一个线程修改了这个collection(或者你自己在循环里修改了- -)。以arraylist为例,实现机制主要是在next(),remove()的过程中调用checkForComodification()。checkForComodification()主要检查modCount == expectedModCount。expectedModCount 是不会变的,只有modCount会变。

这个具体可以看这篇文章

Iterator的最佳实践

Iteraotr的遍历一般可以用while(iter.hasnext())for(:)的方式,这里如果在其中有写操作的话一般有两种方法。

方案一:在遍历过程中所有涉及到改变 modCount 值得地方全部加上 synchronized 或者直接使用 Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

方案二:使用 CopyOnWrite方式,比如使用CopyOnWriteArrayList 来替换 ArrayList。

另外这里简单提一下iterator的remove。我不太记得在哪里看到的资料,但是remove的最佳使用方法是

1
2
3
4
5
6
7
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry)iter.next();
System.out.println(entry.getKey());
if (entry.getKey() == "something") {
iter.remove();
}
}

这里要注意每个remove要对应一个next,否则会报java.lang.IllegalStateException


本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证,欢迎转载,但转载请注明来自http://thousandhu.github.io,并保持转载后文章内容的完整。本人保留所有版权相关权利。

本文链接:http://thousandhu.github.io/2016/06/08/迭代器访问Collection与fail-fast机制/