ConcurrentModificationException
在 Java 中,ArrayList 是一个使用 Fail-Fast 机制的例子。
ConcurrentHashMap是一个使用 Fail-Safe机制的例子。
Fail-Fast
机制,当在迭代过程中检测到集合被修改(添加、删除元素)时,立即抛出ConcurrentModificationException异常,以避免出现并发修改导致的不确定性行为。在Java中,
Fail-Fast
和Fail-Safe
是两种不同的迭代器(Iterator)机制,它们主要涉及到在遍历集合时对集合的修改的处理方式。以下是它们的简要介绍:
- Fail-Fast(快速失败)机制:
定义: 当在迭代过程中检测到集合被修改(添加、删除元素)时,立即抛出
ConcurrentModificationException
异常,以避免出现并发修改导致的不确定性行为。实现: 基于集合(
AbstractList
)内部维护的一个"modCount"变量,该变量记录了集合被修改的次数。在每次迭代开始时,迭代器会自己维护一个"expectedModCount"变量,初始值取自集合的"modCount"变量,在迭代过程中,如果"modCount"和"expectedModCount"不一致,就抛出异常。public class ArrayList<E> extends AbstractList<E> implements List<E> { // ... //继承自AbstractList,写在这里便于理解 private int modCount = 0; // ... public Iterator<E> iterator() { return new Itr(); } //迭代器 private class Itr implements Iterator<E> { int cursor; // 当前元素的索引 int expectedModCount = modCount; // 迭代器创建时的集合修改次数 // ... public E next() { checkForComodification(); // 检查是否有并发修改 // 返回下一个元素 } final void checkForComodification() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } } // ... //集合的add方法 public boolean add(E e) { modCount++; // 添加元素的实现 } // ... }
应用: 主要用于单线程环境,以尽早发现并发修改问题,帮助开发者排除错误。
错误示例:
public static void main(String[] args) { List<String> myList = new ArrayList<>(); myList.add("Item1"); myList.add("Item2"); myList.add("Item3"); Iterator<String> iterator = myList.iterator(); while (iterator.hasNext()) { String item = iterator.next(); //2.抛出ConcurrentModificationException myList.remove(item); //1.在迭代过程中删除元素 } }
- Fail-Safe(安全失败)机制:
定义: 允许在迭代过程中对集合进行修改,但不会抛出异常。迭代器仅仅对原始集合的一个快照进行操作,而不会直接操作原始集合。
实现: 通常使用迭代器的副本或者复制的集合来遍历,这样就不会受到原始集合的修改影响。
//ConcurrentHashMap源码较为繁琐。此处略过
应用: 主要用于多线程环境,以确保在遍历集合时不会被其他线程的修改所干扰。
总体来说,选择使用哪种机制取决于具体的应用场景。
- 在单线程环境下,fail-fast机制可以帮助尽早发现错误。
- 在多线程环境下,fail-safe机制可以提供更好的并发性能和稳定性。在选择时,需要根据应用的需求和性能要求进行权衡。
因无法区分空值和键不存在两种情况
以下仅供参考
HashMap可以有null值,HashMap因为不需要保证多线程环境下的线程安全问题。所以只需要考虑单线程环境下能否区分空值和键不存在两种情况。显然,是可知的
多线程环境下,ConcurrentHashMap无法区分空值和键不存在两种情况,即使当前线程key不为null,也可能在get时被其他线程修改为null
有内存泄漏风险
性能问题
双括号初始化集合是指在集合初始化的时候使用两层花括号的形式,比如:
List<String> list = new ArrayList<String>() {{ add("item1"); add("item2"); }};
虽然这种语法可以实现在初始化时直接添加元素,但通常不被推荐使用,原因如下:
匿名内部类的创建: 双括号初始化实际上创建了一个匿名内部类的实例。每次使用双括号初始化时,都会创建一个新的类,这可能会导致类数量增加,增加类加载和内存开销。
继承关系: 双括号初始化实际上创建了一个匿名内部类,该类继承了指定集合类。这可能导致继承关系的混乱和不必要的复杂性。
内存泄漏风险: 由于创建了匿名内部类,如果在初始化时引用了外部类的实例,那么可能会导致对外部类实例的引用,从而增加了内存泄漏的风险。
性能问题: 双括号初始化的方式可能对性能造成一些微小的影响,尤其是在大型集合的情况下。
相对而言,使用正常的初始化方式更为清晰、简洁,并且没有上述的问题:
List<String> list = new ArrayList<>(); list.add("item1"); list.add("item2");
如果你想在一行代码中初始化和添加元素,可以使用
Arrays.asList
:List<String> list = new ArrayList<>(Arrays.asList("item1", "item2"));
这种方式更为简洁,不引入额外的复杂性和潜在的问题。所以,一般来说,推荐使用正常的初始化方式来创建和初始化集合。