fail-fast和fail-safe的区别:
fail-safe允许在遍历的过程中对容器中的数据进行修改,而fail-fast则不允许。
fail-fast ( 快速失败 )
fail-fast:直接在容器上进行遍历,在遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常导致遍历失败。java.util包下的集合类都是快速失败机制的, 常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList等。
在使用迭代器遍历一个集合对象时,比如增强for,如果遍历过程中对集合对象的内容进行了修改(增删改),会抛出ConcurrentModificationException 异常.
fail-fast的出现场景
在我们常见的java集合中就可能出现fail-fast机制,比如ArrayList,HashMap。在多线程和单线程环境下都有可能出现快速失败。
1、单线程环境下的fail-fast:
ArrayList发生fail-fast例子:
public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0 ; i < 10 ; i++ ) { list.add(i + ""); } Iterator<String> iterator = list.iterator(); int i = 0 ; while(iterator.hasNext()) { if (i == 3) { list.remove(3); } System.out.println(iterator.next()); i ++; } }
该段代码定义了一个Arraylist集合,并使用迭代器遍历,在遍历过程中,刻意在某一步迭代中remove一个元素,这个时候,就会发生fail-fast
2、多线程环境下:
public class FailFastTest { public static List<String> list = new ArrayList<>(); private static class MyThread1 extends Thread { @Override public void run() { Iterator<String> iterator = list.iterator(); while(iterator.hasNext()) { String s = iterator.next(); System.out.println(this.getName() + ":" + s); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } super.run(); } } private static class MyThread2 extends Thread { int i = 0; @Override public void run() { while (i < 10) { System.out.println("thread2:" + i); if (i == 2) { list.remove(i); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } i ++; } } } public static void main(String[] args) { for(int i = 0 ; i < 10;i++){ list.add(i+""); } MyThread1 thread1 = new MyThread1(); MyThread2 thread2 = new MyThread2(); thread1.setName("thread1"); thread2.setName("thread2"); thread1.start(); thread2.start(); } }
fail-fast的原理:
fail-fast是如何抛出ConcurrentModificationException异常的,又是在什么情况下才会抛出?
我们知道,对于集合如list,map类,我们都可以通过迭代器来遍历,而Iterator其实只是一个接口,具体的实现还是要看具体的集合类中的内部类去实现Iterator并实现相关方法。这里我们就以ArrayList类为例。在ArrayList中,当调用list.iterator()时,其源码是:
public Iterator<E> iterator() { return new Itr(); }
避免fail-fast的方法:
方法1
在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法
方法2
使用fail-safe机制,使用java并发包(java.util.concurrent)中的CopyOnWriterArrayList类来代替ArrayList,使用 ConcurrentHashMap来代替hashMap。
fail-safe ( 安全失败 )
fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。
原理:
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。