初识fail-fast:
fail-fast 机制是java集合(Collection)中的一种错误机制
,当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件,它只是一种错误检测机制,只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生
fail-fast的作用:
当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程A访问集合时,就会抛出
ConcurrentModificationException异常,产生fail-fast事件
举例:
准备实体类student:
public class Student {
String name;
public Student(String name){
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
准备测试类:
import java.util.ArrayList;
public class MyArrayList {
public static void main(String[] args) {
ArrayList<Student> arrayList=new ArrayList<>();
arrayList.add(new Student("A"));
arrayList.add(new Student("B"));
arrayList.add(new Student("C"));
arrayList.add(new Student("D"));
for(Student student:arrayList){
System.out.println(student);
}
System.out.println(arrayList);
}
}
第一步:设置断点
第二步:对测试类进行debug操作
第三步:查看当前输出结果
第三步:修改当前程序
第四步:使程序继续运行
fail-fast实现原理:
迭代器在遍历过程中是直接访问内部数据的,因此,内部的数据在遍历的过程中无法被修改,为了保证不被修改,迭代器内部维护了一个标记 “modCount ” ,当集合结构改变(添加删除或者修改),标记"modCount "会被修改
,而迭代器每次的hasNext()和next()方法都会检查该"modCount "是否被改变,当检测到被修改时,抛出Concurrent Modification Exception
我们知道for循环的本质也是迭代,由此设置断点:
查看Iterator的源码
,如下所示:
//modCount用来记录list修改的次数,每修改一次(添加/删除等操作),将modCount+1
protected transient int modCount = 0;
//返回list对应迭代器,实际上,是返回iterator对象
public Iterator<E> iterator() {
return new Itr();
}
//Itr作为实现类实现Iterator接口
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
//在迭代器初始化过程中会将modCount赋给迭代器的expectedModCount.目的是了在迭代过程中,通过next()方法会判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了list,则会抛出ConcurrentModificationException异常,调动fail-fast机制
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
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];
}
.....
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
通过上面额源码我们学习到,若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件,那么什么时候modCount 不等于 expectedModCount?
测试方法如下:
第一步:添加断点—>debug测试类
第二步:运行程序
第三步:debugger处理:
添加成功后该值变为5:
第四步:在调用checkForComodification方法时,添加断点
继续运行程序:
我们再去查看modCount和expectedModCount的大小关系如下:
fail-safe:
发现遍历的同时其他人来修改,应当有应对策略
,例如:牺牲一致性来让整个遍历运行完成
fail-safe的作用:
采用fail-safe机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
举例:
实体类不变还是上述fail-fast中的Student类
测试类代码如下:
import java.util.concurrent.CopyOnWriteArrayList;
public class MyArrayList {
public static void main(String[] args) {
CopyOnWriteArrayList<Student> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add(new Student("A"));
copyOnWriteArrayList.add(new Student("B"));
copyOnWriteArrayList.add(new Student("C"));
copyOnWriteArrayList.add(new Student("D"));
for(Student student:copyOnWriteArrayList){
System.out.println(student);
}
System.out.println(copyOnWriteArrayList);
}
}
重复和上述fail-fast相同的步骤!
添加条件断点:
debug测试类:
继续运行程序:查看输出结果
虽然这种方法并没有报错,且添加的元素都能够被运行,但是我们会发现在遍历的过程中,并没有输出元素E,而在最后打印列表元素时,才有元素E的输出,其实这种情况就称为通过牺牲一致性来保证整个列表的遍历
fail-safe实现原理:
fail-safe任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException
步骤与fail-fast相同,这里我们只说不同的地方!
查看COWIterator源码:
当我们添加元素之后查看array数组中的元素,发现目前存在5个元素:
但是迭代器中的元素依然为4个:
这说明遍历时的数组和迭代的数组不是同一个!
查看add源码,如下所示:
点击setArray源码,如下所示:
最终会将元素添加完成后的数组输出,因此遍历出的数组包含新添加的元素
fai-fast与fail-safe小结:
ArrayList是fail-fast的典型代表,遍历的同时不能修改,否则会抛出异常
CopyOnWriteArrayList是fail-safe的典型代表,遍历的同时可以修改,原理是读写分离
fail-safe机制的缺点:
需要复制集合,产生大量的无效对象,开销大
无法保证读取的数据是目前原始数据结构中的数据