这些都是java集合遍历主题在多线程下的问题以及应对方法。之前一直看到有将fail-fast机制的原因,本身也遇到过concurrentmodificationexcepiton异常,但是总感觉理解不透彻,这次做一个小结把。
要搞清楚这些首先必须回顾一下集合的迭代器设计模式。这里不会涉及具体的实现细节,因为已经有很多很好的文章介绍实现了。
为什么需要迭代器模式?我们不是可以自行写一个for循环做迭代吗?很简单也很方便,何必去使用一个itrerator呢,对于新手来说更不友好。理由是这样的,需要遍历的集合是有具体的类型的,比方说一会是arraylist,一会是set,一会是map,由于每一种具体的集合实现是不同的,导致了它们的遍历方式也不同,最典型的比如arraylist和hashmap的遍历方式就不同。这样当我们拿到一个集合就必须知道它的具体的细节或者类型才能遍历,事实上,对于遍历这个事情,我们可以做一层抽象,只关注与遍历相关的事情,那无非就是getNext()和hasNext(),用这两个方式结合一个while循环就可以完成遍历,这是抽象层次的概念,这样,我们不需要关注底层的集合是什么样的,只要使用刚才的next方法就可以遍历。要实现这层抽象,就必须让底层的每一种集合都实现特定的next类型的方法,具体的办法是把next相关的方法定义在迭代器接口中,每一种集合方法都提供迭代器,并且提供迭代的实现。所以在迭代器模式中有两种角色,一个是迭代器,包括接口和实现,接口定义了迭代方式,实现部分由各种具体的集合类型提供各自的实现。另一个是集合角色,包括接口和实现,接口就是要求集合返回一个迭代器。
所以迭代器模式提供了一种统一的与集合实现方式无关的遍历接口。
在java中,使用foreach语法和显示的itrerator迭代都会触发上述迭代器模式,使用for(int i = 0,i<10;i++)这样的方式不会触发迭代器。
接下来是迭代器在多线程下的可能的问题。在一个线程遍历一个集合的时候,是不允许其他线程做改变集合结构的修改的,比如说添加删除这样的操作的。这是因为这会使得那次迭代的结果无法正常进行。
举个例子,假设一个线程遍历到第十个元素了,另一个线程直接清空了list,那么遍历的线程继续执行就会有错,因为原本正常的索引值9这时是越界了,会引发异常情况,所以一旦发生这样的情况,java如何处理?这就与标题中的两个词相关了。
其实之前看过很多关于这个问题的描述,基本都是复制粘贴,我这里找了一个英文版的解释,感觉十分清楚。
原文地址:http://javaconceptoftheday.com/fail-fast-and-fail-safe-iterators-in-java-with-examples/
java处理遍历多线程中发生修改的策略有两种,一个是fail-fast一个是fail-safe。fail-fast指的是一旦检测到这种情况,立刻停止,会抛出异常concurrentmodificationexcepiton,不会让这种异常状态继续执行,所以叫做“fast”。fail-safe则是继续执行,因为它使用的是另一套机制,是基于一个clone以后的集合遍历的,所以就算原来的集合被其他线程修改了也不会影响遍历。arraylist,vector和hashmap使用fail-fast,concurrenthashmap使用fail-safe机制,下面是一个对比:
了解了概念以后,再看看fail-fast具体是如何实现的,以arraylist为例子。
基本的思路是这样的,需要在每一个集合对象内部维护一个整形变量,通常叫做 modcount。每一次基于集合结构的修改都会让modcount自增。当一个线程要遍历一个集合时,会在迭代器内部也创建一个变量叫expectmodcount,这个变量的值被初始化为集合的modcount变量的值,那么如果集合有结构修改,modcount会改变,但是迭代器内部的expectmodcount仍然是之前的值,这样二者就不相等了,迭代器内部会做这样的检测,具体是在next方法内部,一旦发现不等,就会抛出concurrentmodificationexcepiton异常,终止这次遍历。值得注意的是,单说删除操作,如果调用迭代器的remove方法,内部是会修改expectmodcount变量的,这样二者仍然相等,就不会抛异常了。
下面是arraylist部分实现代码:
private void ensureCapacityInternal(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
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; // Let gc do its work
return oldValue;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
}
/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
以上是arraylist关于修改结构的代码,可以看到都有modcount++的代码。下面看看迭代器里面的代码:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
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();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
是以内部类实现的,可以看做是一个内部类的应用,因为迭代器要依附于具体的集合类。实现里面也确实把expectedModCount的值赋为集合的modCount,每一次的next都会检测二者是否相等,具体在checkForComodification里面,一旦不等抛出异常。而且迭代器里的remove方法是修改了expectedModCount的,所以不会异常。