Java集合系列04之fail-fast机制分析

系列文章

  • Java集合系列01之概览
  • Java集合系列02之ArrayList源码分析
  • Java集合系列03之LinkedList源码分析
  • Java集合系列04之fail-fast机制分析
  • Java集合系列05之Vector&Stack源码分析及List总结
  • Java集合系列06之Map接口概览
  • Java集合系列07之HashMap源码分析
  • Java集合系列08之WeakHashMap源码分析
  • Java集合系列09之TreeMap源码分析
  • Java集合系列10之Hashtable源码分析

前言

fail-fast机制是Java集合的一种错误机制,当多个线程对同一集合内容操作时,便可能会出现fail-fast现象。例如:存在两个线程(A、B),A通过Iterator在遍历集合C中的元素过程中,B修改了集合C的内容,程序就可能会抛出 ConcurrentModificationException 异常,出现fail-fast现象。

本文源码分析基于jdk 1.8.0_121

实例展示

下面给出一个实际代码展示,来演示fail-fast现象,如下:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Main {

    private static List list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i= 0; i<5; i++){
            list.add(i);
        }
        new ThreadA().start();
        new ThreadB().start();
    }

    static class ThreadA extends Thread{
        public void run(){
            Iterator iter = list.iterator();
            int value = 0;
            while(iter.hasNext()) {
                value = (int)iter.next();
                System.out.println(value+", ");
                try{
                    Thread.sleep(10);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    static class ThreadB extends Thread{
        public void run(){
            for (int i = 5;i<10;i++){
                list.add(i);
            }
        }
    }
}

运行以上代码,结果如下:

0,
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 Main$ThreadA.run(Main.java:22)

需要注意的是,如果我们再加一个子线程C,其代码如下:

static class ThreadC extends Thread{
    public void run(){
        for (int i = 0;i<5;i++){
            list.set(i,i+1);
        }
    }
}

new ThreadA().start();
new ThreadC().start();

当我们同时运行子线程A和子线程C时,程序可以顺利执行,因为子线程没有改变原有list的结构,只是重新给每个索引设置了新的值,运行结果如下:

0, 
2, 
3, 
4, 
5, 

解决办法

对于前面的集合ArrayList其不是线程安全的,LinkedList也不是线程安全的,在多线程的环境下,我们可以使用java.util.concurrent包下的类去取代java.util包下的类来避免并发带来的fail-fast现象;对于ArrayList我们可以使用CopyOnWriteArrayList代替即可。

fail-fast原理

当操作Iterator时,如果抛出ConcurrentModificationException便出现了fail-fast现象,而ArrayListiterator方法返回一个Itr对象,Itr类是ArrayList的内部类:

// Itr是迭代器的实现类
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
    
    // 修改次数的记录值
    // 遍历list元素时,会比较expectedModCount和modCount是否相等
    // 如果不相等,则会抛出异常,出现fail-fast现象
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        // 判断expectedModCount和modCount是否相等
        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 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();
    }
}

modCountAbstractList中的一个成员变量,而ArrayList继承自AbstractListArrayList的以下方法会修改modCount

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

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;
}

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; // clear to let GC do its work
}


public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                     numMoved);

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}

...

从以上我们可以发现,当涉及到修改ArrayList中元素个数的方法(remove(),clear(),ensureExplicitCapacity()...),便会修改modCount

因此在前文演示代码中,子线程A正在通过迭代器遍历集合元素,而子线程B中add()方法修改了集合中元素的数量,导致此时expectedModCountmodCount不相等,因此执行checkForComodification方法时,便会抛出异常。

CopyOnWriteArrayList源码分析

前文说了CopyOnWriteArrayList可以解决集合面对并发,下面我们从源码角度分析下CopyOnWriteArrayList

public class CopyOnWriteArrayList
    implements List, RandomAccess, Cloneable, java.io.Serializable {
    
    ...
    
    public Iterator iterator() {
        return new COWIterator(getArray(), 0);
    }
    
    ...
    static final class COWIterator implements ListIterator {
    
        // 创建COWIterator时,将集合中的元素保存到一个新的数组
        // 当原始集合的数据改变,拷贝数据中的值也不会变化
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        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;
        }
        
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
        
        @SuppressWarnings("unchecked")
        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();
        }
        
        ...
        
    }
}

从以上代码,我们可以看到以下几点:

  • CopyOnWriteArrayList没有继承AbstractList,只是实现了List接口
  • ArrayList返回的Iterator是在AbstractList中实现的,而CopyOnWriteArrayList是自己实现的Iterator
  • CopyOnWriteArrayList的各个方法中不会抛出ConcurrentModificationException异常

参考内容

  • Java 集合系列04之 fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)

你可能感兴趣的:(Java集合系列04之fail-fast机制分析)