遍历一个集合,我们有三种办法:
- for循环
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
- foreach
for (int i : list) {
System.out.println(i);
}
- 迭代器Iterator
while (iterator.hasNext()){
int next = (int) iterator.next();
System.out.println(next);
}
单线程情况下这三种遍历都能正常运行,而迭代器的方式在数据量大的情况下是很快的。
看一下Iterator的源码
public interface Iterator{ //如果迭代有更多元素,则返回true boolean hasNext(); //返回迭代中的下一个元素 E next(); //从底层集合中移除此迭代器返回的最后一个元素(可选操作)。 每次调用next只能调用此方法一次。 如果在迭代正在进行时以除调用此方法 以外的任何方式修改了基础集合,则迭代器的行为是未指定的 default void remove() { throw new UnsupportedOperationException("remove"); } //对每个剩余元素执行给定的操作,直到处理完所有元素或操作引发异常。 如果指定了该顺序,则操作按迭代顺序执行。 动作抛出的异常被转发给调用者。 default void forEachRemaining(Consumer super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
我们通常使用的只有hasNext()方法和next()方法,如果我们在迭代时调用remove会怎样呢?
public static void main(String[] args) {
ArrayList list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
int next = (int) iterator.next();
iterator.remove();
}
System.out.println(list.size());;
}
可以看到ArrayList里面的元素被删除了,源码中提出“以此方式以外的修改集合元素则迭代器的行为不确定",那我们来试一下用另一个线程迭代时改动。
private static Listlist = new ArrayList<>(); private static class thread1 extends Thread { @SneakyThrows @Override public void run() { Iterator iterator = list.iterator(); while (iterator.hasNext()) { int i = iterator.next(); System.out.println("thread1遍历:" + i); Thread.sleep(100); } } } private static class thread2 extends Thread { @SneakyThrows @Override public void run() { Thread.sleep(200); list.remove(9); } } public static void main(String[] args) { for (int i = 0; i < 10; i++) { list.add(i); } new thread1().start(); new thread2().start(); }
线程1遍历时让线程2去修改列表元素。
我们发现报了并发修改异常,因此可以初步了解fail-fast,就是程序对集合进行迭代时,其它线程对集合进行了结构的修改(比如删除、添加元素)。当检测到对象的并发修改,但只是抛出该异常并不能阻止,有的时候甚至不报异常,所以我们只能把ConcurrentModificationException当做一个信息,不能针对只报了这个异常就只去检查多线程的问题,其它情况也可能会造成集合元素修改。
以ArrayList为例,我们看看它如何实现Iterator的
private class Itr implements Iterator{ int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} 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(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
我们发现这些方法里面都调用了checkForComodification();
而这个方法里面只有一个if (modCount != expectedModCount)
所以上面为什么并发修改异常的关键就在这里了。
expectedModCount 是在Itr中定义的:
int expectedModCount = ArrayList.this.modCount;
所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:
protected transient int modCount = 0;
那它什么时候会改变呢?来接着往下看源码
public boolean add(E paramE) {
ensureCapacityInternal(this.size + 1);
}
private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1;
}
public boolean remove(Object paramObject) {
int i;
if (paramObject == null)
for (i = 0; i < this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else
for (i = 0; i < this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i);
return true;
}
return false;
}
private void fastRemove(int paramInt) {
this.modCount += 1;
}
public void clear() {
this.modCount += 1;
}
因此我们就明白了,集合的结构改变会引起modCount改变从而导致迭代时产生异常。所以fail-fast的原理就出来了:
当使用迭代器遍历集合时,遍历期间对集合本身的结构的修改会导致modCount增大,和expectedModCount 不一致导致报出ConcurrentModificationException
目前我了解的有以下两种方案:
- 对涉及modCount的地加上synchronized锁或者直接使用 Collection synchronizedList,但是对于加锁操作不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
- 使用CopyonWriteArrayList 替换 ArrayLIst(fail-safe机制),对于map可以使用CurrentHashMap(fail-safe机制)
根据名字也可以看出,CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是先对底层数组复制一次,然后对复制数组进行操作,这样每个线程操作自己复制的那一份数组,自然就不会产生冲突了。 但是由于需要复制数组所以该类产生的开销比较大,比较适用下面两种情况:
-
在不能同步遍历,但又需要从并发线程中排除冲突时。
-
当遍历操作的数量大大超过可变操作的数量时。
1、fail-safe 任何对集合结构的修改都会在一个复制后的新集合上进行,因此不会产生冲突。
2、fail-safe 机制有两个缺点:
(1)需要复制集合,产生大量的无效对象,开销大。
(2)无法保证读取到的数据是目前原始结构中的数据。
3、使用示例:
public static void main(String[] args) {
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
list.add(4);
}
System.out.println(list.size());
}
我们可以看到遍历只输出了1 2 3,而4添加三次后size变成6
这两个机制主要是在hashmap可能会问到,hashtable也是fail-fast的,大家可以再去了解一下。



