文章目录
- 1. fail-fast与fail-safe概述
- 2. fail-fast源码分析
- 3. fail-safe源码分析
- 4. 总结
1. fail-fast与fail-safe概述
快速失败(fail-fast)
,快速失败是Java集合的一种错误检测机制。
- 出现场景:线程A在使用迭代器遍历一个集合对象的时候,线程B对集合对象的内存进行了操作(增加、删除、修改),这时候会抛出
Concurrent Modification Exception
- 原理:就拿
ArrayList
来说,ArrayList
继承了一个抽象类AbstractList
,这个抽象了中有一个成员变量protected transient int modCount = 0;
,这个变量是记录集合被修改的次数的。集合使用迭代器进行遍历的时候,每当迭代器使用hashNext()/next()
之前,会先检测modCount
变量是否为expectedmodCount
值,是的话就遍历;否则抛出异常。 - 注意:这里抛出异常的判断条件为
modCount
变量是否为expectedmodCount
值,如果集合发生变化时,modCount
值刚好又设置为了expectedmodCount
值。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。 - 场景:
java.util
包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如ArrayList
类。
安全失败(fail—safe)
,采用安全失败的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
- 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发
Concurrent Modification Exception
。 - 缺点:基于拷贝内容的优点是避免了
Concurrent Modification Exception
,但同样地,迭代器并不能访问到修改后的内容,也就是说迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 - 场景:
java.util.concurrent
包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList
类。
2. fail-fast源码分析
测试代码,使用IDEA进行debug
public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);for (Integer integer : list) {System.out.println(integer);}
}
这里的增强for循环实际上就是使用迭代器进行迭代的。迭代器记录了当前集合的修改次数。
int expectedModCount = modCount;
在for循环之前,list
集合中添加了四个元素,所以在list
中的modCount
的值为4。
使用IDEA的debug工具,在程序执行过程中往list
集合中添加一个元素——5,模拟并发。
成功往集合中添加第五个元素。
这时候,当下一次增强for
循环执行的时候,也就是使用迭代器的hasNext
方法
public boolean hasNext() {return cursor != size;
}
然后执行迭代器的next
方法
public E next() {//检验集合是否被修改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];
}
首先,会调用checkForComodification()
,检验集合是否被修改,如果修改,那么集合中的mod
不会与expectedModCount
相等。
final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();
}
mod
与expectedModCount
不相等,则会抛出ConcurrentModificationException
异常,程序中断。
3. fail-safe源码分析
使用CopyOnWriteArrayList
进行测试。
public static void main(String[] args) {CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();list.add(1);list.add(2);list.add(3);list.add(4);for (Integer integer : list) {System.out.println(integer);}
}
先来简单阅读一下CopyOnWriteArrayList
的源码
//底层存放数据的数组
private transient volatile Object[] array;
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();}
}final Object[] getArray() {return array;
}final void setArray(Object[] a) {array = a;
}
add
方法,它底层实现是加了ReentrantLock
锁。
首先获取底层数组的长度。然后进行扩容。接着将需要添加进集合的元素放置在扩容后的数组的末端。
再将新数组newElements
赋值给底层数组array
。
接下来在增强for
循环打断点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrm6VV5A-1675313411939)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/9944/image-20230202123227242.png)]
在准备开始增强for
循环的时候,会调用iterator
创建一个COWIterator
迭代器。
public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}
并且将底层存放数据的数组array
作为参数。
//由后续调用next返回的元素的索引。
private int cursor;
//快照,存储的是迭代器进行迭代的时候,集合的数据
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;
}
然后调用hasNext
方法
public boolean hasNext() {return cursor < snapshot.length;
}
接着,调用next
方法,将快照的数组依次输出。
public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];
}
假如这时候,线程B向集合中添加一个元素。但是迭代器中的snapshot
并没有变化。
因为能snapshot
在COWIterator
初始化的时候已经固定了。即使接下来array
发生改变。snapshot
也依然不变。
所以迭代器输出的数据是迭代器创建那一刻之前的数据,迭代过程中无论集合原数据变成什么,都是输出旧数据。
4. 总结
ArrayList
是fail-fast
的经典代表,遍历的同时不能修改,否则会抛出异常。CopyOnWriteArrayList
是fail-safe
的经典代表,遍历的同时可以修改,原理是读写分离。