概括
前面我们学习过了ArrayList 和 LinkedList,它们的迭代器都是快速失败的,也就是”fail-fast”机制。本文将以ArrayList为基础,对迭代器的”fail-fast”机制进行系统地学习。内容主要分为下面几个部分:
- fail-fast简介
- fail-fast示例
- fail-fast解决方法
- fail-fast原理
- fail-fast解决原理
fail-fast简介
fail-fast是Java集合(Collection框架)中的一种错误机制。当多个线程对同一个集合的内容进行操作的时候,就可能会产生fail-fast事件。
例如:当线程A通过迭代器访问集合的过程中,线程B改变了集合的内容;那么当线程A访问集合的时候,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
fail-fast示例
“Talk is cheap, Show me the code!”,为了更进一步认识fail-fast机制,下面将会编写一个示例:
结果:
结果分析:
- FailFastTest通过启动来线程去操作list,ThreadOne往list里写入0-9,每写一个值立马遍历一次,ThreadTwo往list写入10-19,每写一个值也同样立马遍历一次list。
- 当某个线程遍历list的过程中,list被另一个线程改变了,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
fail-fast解决办法
fail-fast机制,是一种错误检测机制(不是容错机制)。它只能用来检测错误,因为JVM并不保证fail-fast一定会发生。如果要在多线程环境中使用fail-fast机制的集合,建议使用java.util.concurrent包的类去替换java.util包的类。
上个示例,只需要把
修改为
就可以解决该问题。
fail-fast原理
产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。那么,ArrayList是如何确定需要抛出ConcurrentModificationException异常的呢?
查看源码,我们可以看见ConcurrentModificationException是在操作Iterator的时候才抛出的(JDK1.8有几个额外的接口也会抛出,比如replaceAll(UnaryOperator
从源码可以看出,无论是next(),remove()等,只要涉及到修改集合元素个数的操作,都会修改modCount的值。
接下来,我们再一步步分析是怎么产生fail-fast事件的:
- new了一个ArrayList,命名为list;
- 向list中添加元素;
- 启动线程A,并在线程A通过迭代器反复遍历list的元素;
- 启动线程B,并在线程B中删除其中的一个节点e;
- 这时,就有个有趣的事情发生了:在某个时刻,线程A创建list的迭代器。此时节点e还在list中,创建迭代器的时候,expectedModCount=modCount,假设为N。在线程A遍历list的某个时刻,线程B执行了删除节点e的操作,此时线程B执行了remove操作后,执行了modCount++,此时modCount = N+1。当线程A继续遍历的时候,expectedModCount还是N,而modCount是N+1,所以便抛出了ConcurrentModificationException异常,产生了fail-fast事件。
至此,我们已经完全理解了fail-fast事件是如何产生的了!
总结来说就是,当某个集合在多线程的环境中被操作时,某线程在访问集合的过程中,该集合的内容被其他线程修改了,导致线程独享的expectedModCount与集合的modCount不一样,这时就会抛出ConcurrentModificationException,产生了fail-fast事件。
fail-fast解决原理
上面说明了fail-fast是如何产生的。接下来,我们再来看看java.util.concurrent包下面的类是如何解决fail-fast事件的。同理,用ArrayList对应的线程安全类CopyOnWriteArrayList来说明。
|
|
从中我们可以看出:
- CopyOnWriteArrayList没有继承AbstractList,而是直接实现List接口;
- CopyOnWriteArrayList创建迭代器的时候,把集合的数组引用直接给赋值给迭代器的snapshot数组,而当执行remove等改变集合元素的时候,会重新创建一个新数组给集合,所以不影响snapshot数组。也就是说,创建迭代器后,迭代器遍历的snapshot数组元素是不变的;
- 综上,当多个线程操作同一个集合的时候,线程A遍历集合的时候就已经把当时集合的所有元素快照下来了,而后线程B修改了集合的内容时,线程A遍历开始时的元素没有发生变化,因此不会抛出ConcurrentModificationException异常,也就不会发生fail-fast事件了。
ListIterator操作会产生fail-fast事件吗
我们知道,ListIterator有remove()
,set()
, add()
方法,这个肯定会影响到列表的modCount属性,那ListIterator的expectedModCount会不会和modCount不相等而导致fail-fat事件呢? 答案是否定的。
我们先看下ListIterator的数据结构:
我们看下add()方法的源码:
从上面可以看出,迭代器的修改方法都会重新执行
所以是不会产生f-f事件的!
关于迭代器的操作,下面有个趣味代码:
其执行结果如何呢?大家可以思考下.结合光标稍微分析下就应该会有答案了呢.