Java中的fail-fast机制(基于JDK1.8)

来源:互联网 发布:plc有仿真软件 编辑:程序博客网 时间:2024/05/12 00:18
fail-fast简介

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

在详细介绍fail-fast机制的原理之前,先通过一个示例来认识fail-fast。

  1. import java.util.*;
  2. import java.util.concurrent.*;
  3. /*
  4. * @desc java集合中Fast-Fail的测试程序。
  5. *
  6. * fast-fail事件产生的条件:当多个线程对Collection进行操作时,若其中某一个线程通过iterator去遍历集合时,该集合的内容被其他线程所改变;则会抛出ConcurrentModificationException异常。
  7. * fast-fail解决办法:通过util.concurrent集合包下的相应类去处理,则不会产生fast-fail事件。
  8. *
  9. * 本例中,分别测试ArrayList和CopyOnWriteArrayList这两种情况。ArrayList会产生fast-fail事件,而CopyOnWriteArrayList不会产生fast-fail事件。
  10. * (01) 使用ArrayList时,会产生fast-fail事件,抛出ConcurrentModificationException异常;定义如下:
  11. * private static List<String> list = new ArrayList<String>();
  12. * (02) 使用时CopyOnWriteArrayList,不会产生fast-fail事件;定义如下:
  13. * private static List<String> list = new CopyOnWriteArrayList<String>();
  14. *
  15. */
  16. public class FastFailTest {
  17. private static List<String> list = new ArrayList<String>();
  18. //private static List<String> list = new CopyOnWriteArrayList<String>();
  19. public static void main(String[] args) {
  20. // 同时启动两个线程对list进行操作!
  21. new ThreadOne().start();
  22. new ThreadTwo().start();
  23. }
  24. private static void printAll() {
  25. System.out.println("");
  26. String value = null;
  27. Iterator iter = list.iterator();
  28. while(iter.hasNext()) {
  29. value = (String)iter.next();
  30. System.out.print(value+", ");
  31. }
  32. }
  33. /**
  34. * 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
  35. */
  36. private static class ThreadOne extends Thread {
  37. public void run() {
  38. int i = 0;
  39. while (i<6) {
  40. list.add(String.valueOf(i));
  41. printAll();
  42. i++;
  43. }
  44. }
  45. }
  46. /**
  47. * 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
  48. */
  49. private static class ThreadTwo extends Thread {
  50. public void run() {
  51. int i = 10;
  52. while (i<16) {
  53. list.add(String.valueOf(i));
  54. printAll();
  55. i++;
  56. }
  57. }
  58. }
  59. }

第一次运行后,出现如下结果(在ThreadOne的代码中抛出异常)。
控制台输出:
0, 10, 
0, 10, 11, 
0, 10, 11, 12, 
0, 10, 11, 12, 13, 
0, 10, 11, 12, 13, 14, 
Exception in thread "Thread-0" java.util.ConcurrentModificationException
0, 10, 11, 12, 13, 14, 15, at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.anyco.failfast.FastFailTest.printAll(FastFailTest.java:36)
at com.anyco.failfast.FastFailTest.access$300(FastFailTest.java:19)
at com.anyco.failfast.FastFailTest$ThreadOne.run(FastFailTest.java:49)


第二次运行后,出现如下结果(在ThreadTwo的代码中抛出异常)。
控制台输出:
Exception in thread "Thread-1" java.util.ConcurrentModificationException

at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
0, 0, 
at java.util.ArrayList$Itr.next(ArrayList.java:851)
0, 1, 
at com.anyco.failfast.FastFailTest.printAll(FastFailTest.java:36)
0, 1, 2, 
at com.anyco.failfast.FastFailTest.access$300(FastFailTest.java:19)
0, 1, 2, 3, 
at com.anyco.failfast.FastFailTest$ThreadTwo.run(FastFailTest.java:63)
0, 1, 2, 3, 4, 
0, 1, 2, 3, 4, 5, 

如果运行时没有出现异常,多运行几次。

结果说明:
在这里list是FastFailTest 的成员变量,我们通过
  1. new ThreadOne().start();
  2. new ThreadTwo().start();
启动了两个新的线程来对list进行修改,ThreadOne向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list,ThreadTwo向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list,当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了,就会抛出ConcurrentModificationException异常,产生fail-fast事件。


为何产生ConcurrentModificationException
由于我们采用的是ArrayList做的测试,所以查看ArrayList源码即可,在上面抛出异常的输出中你会发现异常是出现在checkForComodification方法里面的,具体是在调用ArrayList$Itr.next方法的时候会调用checkForComodification方法,进而产生异常,那么这里的Itr又是什么呢?查看源码发现他是ArrayList的私有内部类,我们在调用ArrayList的iterator方法的时候会创建一个Itr对象出来:

  1. public Iterator<E> iterator() {
  2. return new Itr();
  3. }

  1. private class Itr implements Iterator<E> {
  2. int cursor; // index of next element to return
  3. int lastRet = -1; // index of last element returned; -1 if no such
  4. int expectedModCount = modCount;
  5. public boolean hasNext() {
  6. return cursor != size;
  7. }
  8. @SuppressWarnings("unchecked")
  9. public E next() {
  10. checkForComodification();
  11. int i = cursor;
  12. if (i >= size)
  13. throw new NoSuchElementException();
  14. Object[] elementData = ArrayList.this.elementData;
  15. if (i >= elementData.length)
  16. throw new ConcurrentModificationException();
  17. cursor = i + 1;
  18. return (E) elementData[lastRet = i];
  19. }
  20. public void remove() {
  21. if (lastRet < 0)
  22. throw new IllegalStateException();
  23. checkForComodification();
  24. try {
  25. ArrayList.this.remove(lastRet);
  26. cursor = lastRet;
  27. lastRet = -1;
  28. expectedModCount = modCount;
  29. } catch (IndexOutOfBoundsException ex) {
  30. throw new ConcurrentModificationException();
  31. }
  32. }
  33. @Override
  34. @SuppressWarnings("unchecked")
  35. public void forEachRemaining(Consumer<? super E> consumer) {
  36. Objects.requireNonNull(consumer);
  37. final int size = ArrayList.this.size;
  38. int i = cursor;
  39. if (i >= size) {
  40. return;
  41. }
  42. final Object[] elementData = ArrayList.this.elementData;
  43. if (i >= elementData.length) {
  44. throw new ConcurrentModificationException();
  45. }
  46. while (i != size && modCount == expectedModCount) {
  47. consumer.accept((E) elementData[i++]);
  48. }
  49. // update once at end of iteration to reduce heap write traffic
  50. cursor = i;
  51. lastRet = i - 1;
  52. checkForComodification();
  53. }
  54. final void checkForComodification() {
  55. if (modCount != expectedModCount)
  56. throw new ConcurrentModificationException();
  57. }
  58. }

此句代码int expectedModCount = modCount;很重要,modCount是指list在结构上被改变的次数当然也包括list中内容顺序发生改变的次数,这个值的修改会使得迭代器产生不正确的结果,如果你查看ArrayList中modCount值哪里发生过修改的话,一般出现在add、remove、clear中。
在Itr的三种重要的方法中,都需要checkForComodification。
  1. final void checkForComodification() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. }
由此可知,如果if (modCount != expectedModCount)成立,则会抛出ConcurrentModificationException的异常。expectedModCount的值是我们在开始迭代的时候赋值的,modCount有可能在我们迭代的过程中因为我们向ArrayList中添加或者删除元素而发生变化,这就会导致两者值不等了,也就是modCount != expectedModCount的情况了。
在最开始的demo中,我们用两个线程对同一个List进行操作,倘若在ThreadOne进行遍历list集合时,ThreadTwo对此对象进行了操作,则会导致
modCount != expectedModCount,因此也就抛出了ConcurrentModificationException的异常,ThreadTwo对list集合遍历时,抛出异常原理也如此。


如何解决fail-fast的问题
ArrayList是个线程不安全的类,我们可以通过使用concurrent包下的另一个类CopyOnWriteArrayList来替换它。
  1. public class CopyOnWriteArrayList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  1. public class ArrayList<E> extends AbstractList<E>
  2. implements List<E>, RandomAccess, Cloneable, java.io.Serializable
CopyOnWriteArrayList与ArrayList的重要不同在于,CopyOnWriteArrayList没有继承AbstractList。

对于CopyOnWriteArrayList,其iterator遍历方式如下
  1. public Iterator<E> iterator() {
  2. return new COWIterator<E>(getArray(), 0);
  3. }

  1. static final class COWIterator<E> implements ListIterator<E> {
  2. /** Snapshot of the array */
  3. private final Object[] snapshot;
  4. /** Index of element to be returned by subsequent call to next. */
  5. private int cursor;
  6. private COWIterator(Object[] elements, int initialCursor) {
  7. cursor = initialCursor;
  8. snapshot = elements;
  9. }
  10. public boolean hasNext() {
  11. return cursor < snapshot.length;
  12. }
  13. public boolean hasPrevious() {
  14. return cursor > 0;
  15. }
  16. @SuppressWarnings("unchecked")
  17. public E next() {
  18. if (! hasNext())
  19. throw new NoSuchElementException();
  20. return (E) snapshot[cursor++];
  21. }
  22. @SuppressWarnings("unchecked")
  23. public E previous() {
  24. if (! hasPrevious())
  25. throw new NoSuchElementException();
  26. return (E) snapshot[--cursor];
  27. }
  28. public int nextIndex() {
  29. return cursor;
  30. }
  31. public int previousIndex() {
  32. return cursor-1;
  33. }
  34. public void remove() {
  35. throw new UnsupportedOperationException();
  36. }
  37. public void set(E e) {
  38. throw new UnsupportedOperationException();
  39. }
  40. public void add(E e) {
  41. throw new UnsupportedOperationException();
  42. }
  43. @Override
  44. public void forEachRemaining(Consumer<? super E> action) {
  45. Objects.requireNonNull(action);
  46. Object[] elements = snapshot;
  47. final int size = elements.length;
  48. for (int i = cursor; i < size; i++) {
  49. @SuppressWarnings("unchecked") E e = (E) elements[i];
  50. action.accept(e);
  51. }
  52. cursor = size;
  53. }
  54. }

private final Object[] snapshot;用来保存要遍历的list的快照。
  1. private COWIterator(Object[] elements, int initialCursor) {
  2. cursor = initialCursor;
  3. snapshot = elements;
  4. }
CopyOnWriteArrayList的底层实现仍然是一个数组,新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。


CopyOnWriteArrayList的并发问题
先看CopyOnWriteArrayList的几个重要的接口
  1. public boolean add(E e) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. Object[] elements = getArray();
  6. int len = elements.length;
  7. Object[] newElements = Arrays.copyOf(elements, len + 1);
  8. newElements[len] = e;
  9. setArray(newElements);
  10. return true;
  11. } finally {
  12. lock.unlock();
  13. }
  14. }

  1. public void add(int index, E element) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. Object[] elements = getArray();
  6. int len = elements.length;
  7. if (index > len || index < 0)
  8. throw new IndexOutOfBoundsException("Index: "+index+
  9. ", Size: "+len);
  10. Object[] newElements;
  11. int numMoved = len - index;
  12. if (numMoved == 0)
  13. newElements = Arrays.copyOf(elements, len + 1);
  14. else {
  15. newElements = new Object[len + 1];
  16. System.arraycopy(elements, 0, newElements, 0, index);
  17. System.arraycopy(elements, index, newElements, index + 1,
  18. numMoved);
  19. }
  20. newElements[index] = element;
  21. setArray(newElements);
  22. } finally {
  23. lock.unlock();
  24. }
  25. }

  1. public E remove(int index) {
  2. final ReentrantLock lock = this.lock;
  3. lock.lock();
  4. try {
  5. Object[] elements = getArray();
  6. int len = elements.length;
  7. E oldValue = get(elements, index);
  8. int numMoved = len - index - 1;
  9. if (numMoved == 0)
  10. setArray(Arrays.copyOf(elements, len - 1));
  11. else {
  12. Object[] newElements = new Object[len - 1];
  13. System.arraycopy(elements, 0, newElements, 0, index);
  14. System.arraycopy(elements, index + 1, newElements, index,
  15. numMoved);
  16. setArray(newElements);
  17. }
  18. return oldValue;
  19. } finally {
  20. lock.unlock();
  21. }
  22. }

毫无疑问CopyOnWriteArrayList在添加和remove元素的时候,都通过ReentrantLock加了锁,同一时刻对该list的添加、删除元素只能有一个线程在执行。
对public boolean add(E e)方法来说,同一时间只能有一个线程在添加元素。然后使用Arrays.copyOf(...)方法复制出另一个新的数组,而且新的数组的长度比原来数组的长度+1,副本复制完毕,新添加的元素也赋值添加完毕,最后又把新的副本数组赋值给了旧的数组,最后在finally语句块中将锁释放。
public E remove(int index),删除指定位置的元素,先判断要删除的元素是否最后一个,如果最后一个直接在复制副本数组的时候,复制长度为旧数组的length-1即可;但是如果不是最后一个元素,就先复制旧的数组的index前面元素到新数组中,然后再复制旧数组中index后面的元素到数组中,最后再把新数组复制给旧数组的引用。
最后在finally语句块中将锁释放。


CopyOnWriteArrayList的性能问题
优点:
1.解决的开发工作中的多线程的并发问题。
缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
2.数据一致性:CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上能读到,请不要使用CopyOnWriteArrayList容器,因为每次添加删除元素后,都需要经过一次拷贝数组的过程