上一篇《fail-fast究竟是个什么鬼》我们学习了什么是fail-fast,那么,java.util包下的集合类是如何利用modCount保证fail-fast机制的?那么modCount作为java.util包中的灵魂字段,我觉得就很有必要专门为它写上一篇。接下来,本文将以最常用的ArrayList类作为源码切入点,一起来揭秘modCount的神秘面纱。
先看一下ArrayList的继承实现关系图(idea快捷键 Ctrl + Alt + u 或 Ctrl + Alt + Shift + u )。
在关系图中可以看到,ArrayList继承了AbstractList,在AbstractList中有一个modCount成员变量。
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;
源码中的注释才是最好的一手材料。基于源码写出来的材料,会因为作者的能力而良莠不齐,同样,也会因读者的能力导致这种知识的传递效果迥异。先看一下注释:
这个列表在结构上被修改的次数 。 结构修改是那些改变列表大小的修改,或者以一种方式(这种方式使得进行中的迭代可能产生不正确的结果)扰乱列表。
这个字段被迭代器和列表迭代器的实现所使用,这种实现的实例是通过iterator方法和listIterator方法返回的。如果这个字段的值意外地改变了,那么迭代器(或列表迭代器)在响应next,remove,previous,set或者add操作时将会抛出ConcurrentModificationException。在迭代过程中面对并发修改,这就提供了**fail-fast(快速失败)**行为,而不是非确定的行为。
子类使用这个字段是可选择的。如果子类希望提供fail-fast迭代器(或列表迭代器),那么在add方法和remove方法中(或者该子类重写的任何方法,这些方法会导致列表的结构修改)就不得不增加这个字段的值。单次调用add方法或remove方法必须为这个字段加1,否则迭代器(或列表迭代器)将会抛出虚假的ConcurrentModificationExceptions。如果一种实现不希望提供fail-fast迭代器,那么这个字段就会被忽略。
modCount的使用范围修饰符是protected,所以子类会继承它。transient表示该字段不需要序列化而已。modCount意思是被修改的次数,应该是modifiedCount的缩写。
对于上面的翻译,做更进一步理解。
structurally modified
structurally modified 结构性修改,在一个集合(Collection接口)中什么样的修改,可以称为结构性修改。结构的修改是指改变列表的size的修改,那size是什么?size是一个集合中所含的元素个数,在Collection接口中有个size() 方法就是返回集合的元素个数。
/**
* Returns the number of elements in this collection. If this collection
* contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
* <tt>Integer.MAX_VALUE</tt>.
*
* @return the number of elements in this collection
*/
int size();
那么什么方法会改变一个集合实现类的size呢?当然,是add或remove(以及add和remove衍生的方法)。也是就集合实现类,会在add或remove方法中,会执行modCount++。可以看看ArrayList的add和remove源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
add方法的ensureCapacityInternal(size + 1),后面的注释Increments modCount!!,就是说会自增modCount。remove方法中可以清楚的看到有modCount++(其它add和remove的衍生方法addAll,removeAll也会同样自增modCount)。
同时,注意到后面还有一种关于结构性修改的解释——以一种方式扰乱列表,这种方式会使得进行中的迭代可能产生不正确的结果。从这句话中,我理解到的是这种方式没有改变size的大小,而且这种方式区别于前面说的改变列表size大小的方式,那么,什么方式会在不改变列表size的大小的情况下,会使得迭代过程可能产生不正确的结果 ?说实话,这个问题我想很久没想明白。直到有一天,我看到源码的时一眼扫到了sort方法。刹那间,茅塞顿开——如果一个线程A在迭代输出一个含有1,2,3的ArrayList,另外一个线程B在线程A的迭代的过程中,将ArrayList倒序排序了,那么线程A可能会输出1,2,1,这就造成了size的大小不变,而迭代过程产生了不正确的结果。那么,sort方法中肯定有modCount++。
@Override
@SuppressWarnings("unchecked")
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
同时,属于第二种方式的还有replaceAll方法,retainAll方法,removeIf方法等等。
注意到,在LinkedList的源码注释中,有这样一句话“A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.”。结构性修改是指任何会添加或删除一个或多个元素的操作,仅仅设置一个元素的值不是结构性的修改。因为LinkedList源码中没有ArrayList的第二种方式的那些方法。
changes unexpectedly
changes unexpectedly 意外地改变,是指在使用迭代器和列表迭代器时,这个list的size被改变了,而这种改变不是通过迭代器自己的方法。那么迭代器在响应自身的方法(next,remove,previous,set或者add),会报错ConcurrentModificationException。这句话的意思是,在使用迭代器时要想进行结构性修改必须使用迭代器自身的方法,如果不是,迭代器就认为是发生了意外地改变,就会报错。在也就是明明单线程中,在迭代过程中使用列表的remove方法,没有使用迭代器remove方法会报错ConcurrentModificationException的原因。
在ArrayList的有两个内部类Itr和ListItr,在这两个内部类中有next,remove,previous,set、add方法,这些方法都调用checkForComodification()方法。checkForComodification就是检查是否发生并发修改的方法,其实现原理就是比较modCount和expectedModCount是否相等,如果相等则不是则认为发生了ConcurrentModificationException。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
那expectedModCount(被期望的修改次数)是什么意思?是哪个类的字段?在内部类Itr中,有这样一行代码"int expectedModCount = modCount;",也就是说expectedModCount初始化的时候是取得modCount值。从上面的ArrayList的add和remove源码中,我们知道了modCount会自增,而没有expectedModCount的自增,那么在迭代过程中使用了ArrayList的add和remove方法当然会报错。与此同时,我们注意到Itr也有remove方法,那么在迭代过程中,调用Itr.remove()不会报错,是不是意味着在Itr.remove()修改了modCount的同时,也修改了expectedModCount?让我们一起通过源码来找答案。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
可以清楚的看到“expectedModCount = modCount;”,Itr.remove()方法try语法块中,先调用了ArrayList.this.remove(lastRet)也就是ArrayList的remove方法,modCount会自增。接着执行“ expectedModCount = modCount;”,这样就会将重置expectedModCount ,使得expectedModCount 和modCount保持一致。这样一来,就不会发生ConcurrentModificationException。
fail-fast
维基百科中关于fail-fast的解释:
在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。
其实,这是一种理念,fail-fast就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报。
更多关于fail-fast的内容可以查看《fail-fast究竟是个什么鬼》。
同样地,Set和Map的实现类也会有fail-fast机制,实现原理和List基本相同。java.util包下的集合实现类和Map实现类都实现了fail-fast机制。