今天看到了一个问题,当A线程在查询List时,B线程对该List进行操作,会发生什么?当时脑子里只想着那肯定是不能操作的呀,但是并不知道怎么个不能操作法,后来才想到,List集合是有快速失败机制 的。
fast-fail事件 import java.util.ArrayList;import java.util.List;public class FastFail { public static void main (String[] args) { List<Integer> list = new ArrayList <>(); for (int i = 0 ; i < 10 ; i++) { list.add(i); } new MyThread1 (list).start(); new MyThread2 (list).start(); } } class MyThread1 extends Thread { private List<Integer> list; public MyThread1 ( List<Integer> list) { this .list = list; } @Override public void run () { for (Integer integer : list) { System.out.println("MyThread-A" + list.size()); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyThread2 extends Thread { private List<Integer> list; public MyThread2 ( List<Integer> list) { this .list = list; } @Override public void run () { for (int i = 0 ; i < 10 ; i++) { try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } list.add(i); System.out.println("MyThread-B " + list.size()); } } }
当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,即产生fail-fast事件。
解决方法 fail-fast事件发生的主要原因是线程不安全问题所导致的,为此可以使用线程安全的类来解决这个问题,刚好JDK就为我们提供了CopyOnWriteArrayList。
public class FastFail { public static void main (String[] args) { List<Integer> list = new CopyOnWriteArrayList <>(); for (int i = 0 ; i < 10 ; i++) { list.add(i); } new MyThread1 (list).start(); new MyThread2 (list).start(); } }
将ArrayList替换为CopyOnWriteArrayList后,程序可正常执行。
原理
和ArrayList继承于AbstractList不同,CopyOnWriteArrayList没有继承于AbstractList,它仅仅只是实现了List接口。
ArrayList的iterator()函数返回的Iterator是在AbstractList中实现的;而CopyOnWriteArrayList是自己实现Iterator。
ArrayList的Iterator实现类中调用next()时,会“调用checkForComodification()比较‘expectedModCount’和‘modCount’的大小”;但是,CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常!
由此可见,当 “modCount 不等于 expectedModCount ”,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
在创建Itr对象时,expectedModCount被默认赋值为modCount。
但是在比较中modCount 不等于 expectedModCount,那么问题来了,modCount 是什么时候被改变的呢 ?
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 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 void replaceAll (UnaryOperator<E> operator) { Objects.requireNonNull(operator); final int expectedModCount = modCount; final int size = this .size; for (int i=0 ; modCount == expectedModCount && i < size; i++) { elementData[i] = operator.apply((E) elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException (); } modCount++; } public void sort (Comparator<? super E> c) { final int expectedModCount = modCount; Arrays.sort((E[]) elementData, 0 , size, c); if (modCount != expectedModCount) { throw new ConcurrentModificationException (); } modCount++; }
通过阅读ArrayList源码,我们可以发现,只要是涉及到集合元素加减的操作,例如remove()、add()、replaceAll()、sort()等方法都会修改modCount
分析原因 线程A创建了arrayList的Iterator,此时节点A仍然存在于arrayList中,创建arrayList时,expectedModCount = modCount(假设它们此时的值为N)。
在线程A在遍历arrayList过程中的某一时刻,线程B执行了,并且线程B删除了arrayList中的节点A。线程B执行remove()进行删除操作时,在remove()中执行了modCount++,此时modCount变成了N+1!
线程A接着遍历,当它执行到next()函数时,调用checkForComodification()比较expectedModCount和modCount的大小;而expectedModCount=N,modCount=N+1,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件。
补充:CopyOnWriteArrayList的读写 CopyOnWriteArrayList 读取操作的实现
读取操作没有任何同步控制和锁操作,理由就是内部数组 array不会发生修改,只会被另外一个array替换,因此可以保证数据安全。
private transient volatile Object[] array;public E get (int index) { return get(getArray(), index); } @SuppressWarnings("unchecked") private E get (Object[] a, int index) { return (E) a[index]; } final Object[] getArray() { return array; }
CopyOnWriteArrayList 写入操作的实现
CopyOnWriteArrayList写入操作 add()方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。
public boolean add (E e) { final ReentrantLock lock = this .lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1 ); newElements[len] = e; setArray(newElements); return true ; } finally { lock.unlock(); } }