初识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机制的缺点:
需要复制集合,产生大量的无效对象,开销大 无法保证读取的数据是目前原始数据结构中的数据