Fail-fast mechanism is an error mechanism in Java Collection. When multiple threads operate on the contents of the same collection, fail-fast events may occur. For example, when a thread A traverses a collection through iterator, if the content of the collection is changed by other threads, then thread A will throw a Concurrent ModificationException exception when accessing the collection, resulting in a fail-fast event.
The iterator's fast failing behavior is not guaranteed, it can't guarantee that the error will occur, but the fast failing operation will try its best to throw the Concurrent ModificationException exception.
Note: Above mentioned fail-fast events occur in multi-threaded environments, but fail-fast events occur in single-threaded environments if rules are violated.
There is a passage in the document that it is wrong to write programs that rely on the exception generated by the fast failure mechanism, and the fast detection mechanism of iterators is only used to detect errors.
Two procedures are used to test the causes of the fast failure mechanism.
Single-threaded environment:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Created with IDEA * * @author DuzhenTong * @Date 2018/3/18 * @Time 17:34 */ public class FailFast { public static void main(String[] args) { List list = new ArrayList(); for (int i = 0; i < 10; i++) { list.add(i); } iterator(list); } public static void iterator(List list) { Iterator it = list.iterator(); int index = 0; while (it.hasNext()) { if (index == 6) { list.remove(index); } index++; System.out.println(it.next()); } } }
Output results:
0 1 2 3 4 5 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819) at java.util.ArrayList$Itr.next(ArrayList.java:791) at FailFast.iterator(FailFast.java:29) at FailFast.main(FailFast.java:18)
Multithread environment:
import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Created with IDEA * * @author DuzhenTong * @Date 2018/3/18 * @Time 17:59 */ public class FailFast1 { public static List list = new ArrayList(); public static void main(String[] args) { for (int i = 0; i < 10; i++) { list.add(i); } new ThreadA().start(); new ThreadB().start(); } public static class ThreadA extends Thread { @Override public void run() { Iterator it = list.iterator(); while (it.hasNext()) { System.out.println("In set traversal... :"+it.next()); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static class ThreadB extends Thread { @Override public void run() { int index = 0; while (index != 10) { System.out.println("Thread waiting... :"+index); if (index == 3) { list.remove(index); } index++; } } } }
Output results:
Thread waiting... 0 In set traversal... 0 Thread waiting... 1 Thread waiting... 2 Thread waiting... 3 Thread waiting... 4 Thread waiting... 5 Thread waiting... 6 Thread waiting... 7 Thread waiting... 8 Thread waiting... 9 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819) at java.util.ArrayList$Itr.next(ArrayList.java:791) at FailFast1$ThreadA.run(FailFast1.java:28)
The above program has explained why fail-fast events occur (fast failures). Under multithreaded conditions, one thread is traversing the elements of a collection. When another thread changes the structure of the collection, the program throws a Concurrent ModificationException. In single-threaded conditions, when the structure of the collection is changed, the program will do so. Throw Concurrent ModificationException.
To know exactly why fail-fast occurs, it is necessary to analyze the source code. Fail-fast occurs when traversing a set, that is, when iterating over a set, it is an operation iterator when iterating over a set. The inner class of the set: (ArrayList source code)
private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = modCount;//---------------------1 public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); //... The code here omits... } 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(); } }
In the above code, we can see that the Concurrent ModificationException exception is thrown. The next () and remove () above call the checkForComodification() method to check whether the values of the two variables are equal, and if they are not equal, the exception will be thrown. Number 1 in the above program:
int expectedModCount = modCount;
The value of modCount is assigned to expectedModCount. Do you know what the meaning of modCount is? Why did it change? The reason will be found.
Source point in, found that the modCount variable is not in the ArrayList class, but in AbstractList, as a member variable.
protected transient int modCount = 0;
Next, analyze the source code to see the most commonly used methods
The add method in ArrayList:
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
The ensureCapacityInternal method invoked:
private void ensureCapacityInternal(int minCapacity) { modCount++;//modCount self-increasing———————————— // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); }
The remote method in ArrayList:
public E remove(int index) { rangeCheck(index); modCount++;//modCount self-increasing—————————————— 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; }
The clear method in ArrayList:
public void clear() { modCount++;//modCount self-increasing—————————————— // Let gc do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
The source code is analyzed to see why. Any operation involving changing the structure of the set (changing the number of elements), including adding, removing or emptying, etc., modCount, will increase itself. When an iteration object is obtained, the modCount variable is assigned to expectedModCount first. Whether the variable is expected or not is checked every time during iteration. ModCount is consistent because the value of modCount changes if it is added or deleted in the collection.
Solution:
- For operations involving changing the number of elements in a collection, add synchronized, or force their operations to be synchronized using Collections. synchronized List.
- Replace ArrayList with CopyOnWriteArrayList
There are many articles on the Internet that say: Why can CopyOnWriteArrayList not fail-fast? Because all the variable operations of CopyOnWriteArrayList (add, set, etc.) are implemented by a new copy of the underlying array.
Source code can be analyzed (1, 2 below)
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray();//----1 int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1);//----2 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
My point of view: The reason is not only that CopyOnWriteArrayList adds, sets, removes and other methods that change the original array, but also copy an original array first, then add, set, remove operations on the copy array, which does not affect the COWIterator's array.
Why can memory consistency be achieved without recording the number of modifications or without comparing modCount?
At code 1 above, the getArray() method is called to see the source code:
final Object[] getArray() { return array; }
private volatile transient Object[] array;
Because the type of array returned by getArray() is modified with volatile, volatile type (compulsory memory consistency)
Specifically, you can see my other article on volatile: volatile keyword parsing
Reference article: http://cmsblogs.com/?p=1220