【大白话说Java面试题】【Java基础篇】第2题:Iterator的fail-fast和fail-safe机制有什么区别?

0 阅读3分钟

第2题:Iterator的fail-fast和fail-safe机制有什么区别

📚 回答:

  • 概念

    • fail-fast(快速失败):遍历ArrayList的时候,如果有其他线程偷偷修改了集合(比如增删改),就会直接抛出异常,告诉你“别乱动!”

    • fail-safe(安全失败):遍历CopyOnWriteArrayList的时候,允许你边遍历边修改集合,因为它会自己维护一份副本,用来遍历,而原始集合可以随意修改。

  • 区别

    • fail-fast机制

      • 底层通过一个modCount变量记录集合的修改次数。如果遍历时发现modCount变了,就抛出ConcurrentModificationException
      • 注意:这里说的"其他线程偷偷修改"其实不太准确。fail-fast主要是检测同一个线程在遍历过程中对集合做了结构性修改(比如add、remove),或者其他线程并发修改。但最经典的场景是单线程里自己边遍历边修改,也会抛异常。
      • 示例:
        List<String> list = new ArrayList<>();
        list.add("a");
        for (String s : list) {
            list.add("new"); // 抛出ConcurrentModificationException
        }
        
      • 原理补充:迭代器在创建时会记录当前的modCount,每次调用next()remove()时都会检查modCount是否和预期一致。不一致就说明集合被修改过了,立马抛异常。
    • fail-safe机制

      • 使用CopyOnWriteArrayList时,迭代器会复制一份当前集合的快照用于遍历,而原始集合可以继续被修改。
      • 注意:不是"每次修改都复制整个数组"这么夸张。CopyOnWriteArrayList是写时复制——只有调用add、set、remove这类修改操作时,才会复制一份新数组,在新数组上修改,然后把引用指向新数组。读操作(包括遍历)完全不需要复制,直接读原数组。
      • 示例:
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("a");
        for (String s : list) {
            list.add("new"); // 不会抛异常,但遍历的是旧快照,新加的"new"本次遍历看不到
        }
        

💡 面试官视角:

  • 面试官可能会问“为什么fail-fast会抛出异常?” 答:因为fail-fast通过modCount字段检测集合的结构性修改次数,一旦发现迭代器记录的expectedModCount和实际的modCount不一致,就会认为存在并发修改或非法操作,从而抛出ConcurrentModificationException
  • 面试官可能会追问“fail-safe为什么性能开销大/性能差?” 答:因为写操作(add、remove等)时需要复制整个底层数组,尤其是在数据量较大时,时间和空间开销显著增加。但读操作完全不受影响,所以读多写少的场景才适合用它。
  • 面试官可能会问“实际开发中如何选择这两种机制?” 答:
    • 如果需要高并发支持且读多写少,选择fail-safe机制(如CopyOnWriteArrayListConcurrentHashMap的迭代器也是fail-safe的)。
    • 如果是单线程或者写多读少的场景,用普通的ArrayListHashMap就行,它们都是fail-fast的。但要注意别在遍历过程中修改集合,非要改的话可以用迭代器自己的remove()方法,或者遍历完再改。