Fail-Fast 与 Fail-Safe
- ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
- CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离
ArrayList的底层原理:
ArrayList有个属性
modCount
,每次add()
都让会modCount+1
,remove()
都是modCount-1
; ArrayList 的遍历对象Iterator
维护了一个expectedModCoun
t属性, 执行第一次遍历时会把modCount的
值赋给expectedModCount
, 接着每次遍历都会判断modCount
和expectedModCount
是否相等,如果不相等:说明ArrayList遍历时添加或者删除了元素,此时会抛出ConcurrentModificationException
异常。
CopyOnWriteArrayList的底层原理:
CopyOnWriteArrayList中存储元素的对象时一个
Object[] array
,CopyOnWriteArrayList的遍历对象COWIterator对象
中维护了一个Object[] snapshot
对象。
第一次遍历时便会让snapshot
指向array
的引用,接下来的遍历都是直接遍历snapshot副本。 并且CopyOnWriteArrayList每次添加元素时都会用创建一个新的array
,所以执行遍历过程中如果CopyOnWriteArrayList中并发加入了或者删除了元素,其实此次遍历是无法感知到的,因为CopyOnWriteArrayList遍历其实是对快照对象(snapshot
)的遍历。
Fail-Fast( 快速失败)
java代码
public class FailFastAndFailSafeDemo {
@Test
public void testFailFast() {
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
for (String str : list) {
System.out.println(str);
}
System.out.println(list);
}
}
1.list中有4个元素,第一次进入遍历代码,modCount为4
- 在遍历时,
foreach
其实就是调用了Iteratror对象
中的迭代方法, (foreach其实为语法糖) , 此处将modCount
的值赋给了expectedModCount
,后面如果list中添加数据只会改变modCount
,expectedModCount
并不会变,所以如果遍历时添加和删除元素,会导致modCount != expectedModCount
,接着会抛出异常
3.我们在debug
中用Evaluate
工具给list添加一个元素”E“, 此时继续执行上次遍历,问你们可以发现list的size已经变成了5 ,modCount
也加了1,变成了5;但是此时expectedModCount
还是遍历时赋的值,还是4,所以便会抛出异常.
Fail-Safe(安全失败)
java代码
@Test
public void testFailSafe() {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
for (String str : list) {
System.out.println(str);
}
ListIterator<String> iterator = list.listIterator();
System.out.println(list);
}
1.第一次进入到for循环中,可以看到数据组的长度为4
2. CopyeOnWriteArrayList
中的 foreach遍历真正执行次对象COWIterator
:这里有个新的Object[] snapshot
对象,看名字就知道是list的快照。 当我们执行遍历时会将snapshot的引用指向 list的array
对象,然后用snapshot进行遍历。(看下图snapshot对象
的引用地址和第一步的array一样,都是728)(这种情况下如果遍历时并发加入新的元素,是打印不出来新加入的元素的)
3.这一步用debug中的Evaluate
工具给list添加一个元素”E“,可以看到这里的list中的array
又是个新的对象了 (见下图:现在地址是752,第一步中的是728)。 所以安全失败其实就是每次·add()·都会复制出来一个新的数组对象,然后遍历还是用的老的对象,所以遍历时即使你添加了新的元素,也不会把这个元素打印出来
//add()方法
里的源码,可以看到每次add都会创建了一个新的对象
此时的cursor
为2,说明是第二次进入遍历,但是COWIterator
中的 snapshot
对象的地址还是728.
小tip,degbug时集合类展示成对象,而不仅仅是元素