[TOC]
概要
本文主要以 ArrayList 为例,对 Iterator 的快速失败(fail-fast
), 也就是 Java 集合的错误检测机制进行学习总结。主要内容有:
- 简介
- 错误展示
- 问题解决
- 理解原理
- JDK的解决办法
简介
“快速失败”也就是 fail-fast,它是 Java 集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast
机制。
记住是有可能,而不是一定。例如:假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,
在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),
那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。
错误示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package com.littlefxc.examples.base.collections;
import java.util.ArrayList; import java.util.Iterator; import java.util.List;
public class FailFastTest {
private static List<Integer> list = new ArrayList<>();
private static class threadOne extends Thread {
@Override public void run() { Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { int i = iterator.next(); System.out.println("ThreadOne 遍历:" + i); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
private static class threadTwo extends Thread {
@Override public void run() { int i = 0; while (i < 6) { System.out.println("ThreadTwo run:" + i); if (i == 3) { list.remove(i); } i++; } } }
public static void main(String[] args) { for (int i = 0; i < 10; i++) { list.add(i); } new threadOne().start(); new threadTwo().start(); } }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13
| ThreadOne 遍历:0 ThreadTwo run:0 ThreadTwo run:1 ThreadTwo run:2 ThreadTwo run:3 ThreadTwo run:4 ThreadTwo run:5 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at com.littlefxc.examples.base.collections.FailFastTest$threadOne.run(FailFastTest.java:25)
Process finished with exit code 0
|
问题解决
先说解决办法:
- 在遍历过程中所有涉及到改变 modCount 值得地方全部加上 synchronized 或者直接使用 Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。
- 使用 CopyOnWriteArrayList 来替换 ArrayList。推荐使用该方案。
理解原理
同过上面的错误示例和问题解决,可以初步了解到产生 fail-fast
的原因就在于
当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;
就会抛出 ConcurrentModificationException
异常,产生fail-fast事件。
ConcurrentModificationException
的产生:当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。
也就是说,即便是在单线程环境中,只要违反了规则,同样也可能会抛出异常。
当我对代码运行多次时,发现代码运行有几率不抛出异常,这就说明迭代器的快速失败行为并不能得到保证,所以,不要写依赖这个异常的程序代码。
正确的做法是:ConcurrentModificationException
应该仅用于检测 bug。
AbstractList
抛出 ConcurrentModificationException
的部分代码(Java8):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package java.util;
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected transient int modCount = 0;
public Iterator<E> iterator() { return new Itr(); }
private class Itr implements Iterator<E> { int cursor = 0;
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() { return cursor != size(); }
public E next() { checkForComodification(); try { E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }
public void remove() { if (lastRet == -1) throw new IllegalStateException(); checkForComodification();
try { AbstractList.this.remove(lastRet); if (lastRet < cursor) cursor--; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException e) { throw new ConcurrentModificationException(); } }
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
}
|
从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。
要搞明白 fail-fast机制,我们就要需要理解什么时候“modCount 不等于 expectedModCount”!
从Itr类中,我们知道 expectedModCount 在创建Itr对象时,被赋值为 modCount。通过Itr,我们知道:expectedModCount不可能被修改为不等于 modCount。所以,需要考证的就是modCount何时会被修改。
那么它(modCount)在什么时候因为什么原因而发生改变呢?
ArrayList部分源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
| public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } }
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); }
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); 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; return oldValue; } public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } 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; }
public void clear() { modCount++; for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } }
|
从上面的源代码我们可以看出,ArrayList 中无论 add、remove、clear 方法只要是涉及了改变 ArrayList 元素的个数的方法都会导致 modCount 的改变。
所以我们这里可以初步判断由于 expectedModCount 得值与 modCount 的改变不同步,导致两者之间不等从而产生 fail-fast 机制。
场景还原:
有两个线程(线程 A,线程 B),其中线程 A 负责遍历 list、线程B修改 list。线程 A 在遍历 list 过程的某个时候(此时 expectedModCount = modCount=N),
线程启动,同时线程B增加一个元素,这是 modCount 的值发生改变(modCount + 1 = N + 1)。
线程 A 继续遍历执行 next 方法时,通告 checkForComodification 方法发现 expectedModCount = N ,而 modCount = N + 1,两者不等,
这时就抛出ConcurrentModificationException 异常,从而产生 fail-fast 机制。
至此,我们就完全了解了fail-fast是如何产生的!
也就是,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);
这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
JDK的解决办法:CopyOnWriteArrayList
CopyOnWriteArrayList 是 ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。
该类产生的开销比较大,但是在两种情况下,它非常适合使用。
- 在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
- 当遍历操作的数量大大超过可变操作的数量时。
遇到这两种情况使用 CopyOnWriteArrayList 来替代 ArrayList 再适合不过了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| package java.util.concurrent; import java.util.*; import java.util.concurrent.locks.*; import sun.misc.Unsafe;
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); }
private static class COWIterator<E> implements ListIterator<E> { private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; }
public boolean hasNext() { return cursor < snapshot.length; }
public boolean hasPrevious() { return cursor > 0; }
public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; }
public E previous() { if (! hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; }
public int nextIndex() { return cursor; }
public int previousIndex() { return cursor-1; }
public void remove() { throw new UnsupportedOperationException(); }
public void set(E e) { throw new UnsupportedOperationException(); }
public void add(E e) { throw new UnsupportedOperationException(); } }
}
|
可以从上面的源码中可以看出:
- 和 ArrayList 继承于 AbstractList 不同,CopyOnWriteArrayList 没有继承于 AbstractList,它仅仅只是实现了 List 接口。
- ArrayList 的 iterator() 函数返回的 Iterator 是在 AbstractList 中实现的;而 CopyOnWriteArrayList 是自己实现 Iterator。
- ArrayList 的 Iterator 实现类中调用 next() 时,会“调用 checkForComodification() 比较
expectedModCount
和 modCount
的大小”;但是,CopyOnWriteArrayList 的 Iterator 实现类中,没有所谓的 checkForComodification(),更不会抛出 ConcurrentModificationException 异常!
CopyOnWriterArrayList 的 add 方法与 ArrayList 的 add 方法有一个最大的不同点就在于,下面三句代码:
1 2 3
| Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements);
|
就是这三句代码使得 CopyOnWriterArrayList 不会抛 ConcurrentModificationException 异常。
它们就是 copy 原来的 array,再在 copy 数组上进行 add 操作,这样做就完全不会影响 COWIterator 中的 array 了
CopyOnWriterArrayList 的核心概念就是:
任何对 array 在结构上有所改变的操作(add、remove、clear 等),CopyOnWriterArrayList 都会 copy 现有的数据,再在 copy 的数据上修改,
这样就不会影响 COWIterator 中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,
同时数组的 copy 也是相当有损耗的。