引言

作为一名 Java 开发人员,你一定对 ConcurrentModificationException 不陌生。这是一个在使用迭代器遍历集合对象时,因并发修改集合对象而引发的异常。实际上,Java 集合框架是 迭代器设计模式 的一个典型实现。

Java 1.5 引入了 java.util.concurrent 包,其中 Collection 类 的实现允许在运行过程中修改集合对象。ConcurrentHashMap 是一个与 HashMap 很相似的类,但它支持在遍历过程中安全地修改集合对象。

让我们通过一个简单的程序来深入理解两者的区别。

示例代码

ConcurrentHashMapExample.java

package com.journaldev.util;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

    public static void main(String[] args) {

        // ConcurrentHashMap
        Map<String, String> myMap = new ConcurrentHashMap<String, String>();
        myMap.put("1", "1");
        myMap.put("2", "1");
        myMap.put("3", "1");
        myMap.put("4", "1");
        myMap.put("5", "1");
        myMap.put("6", "1");
        System.out.println("ConcurrentHashMap before iterator: " + myMap);
        Iterator<String> it = myMap.keySet().iterator();

        while (it.hasNext()) {
            String key = it.next();
            if (key.equals("3")) myMap.put(key + "new", "new3");
        }
        System.out.println("ConcurrentHashMap after iterator: " + myMap);

        // HashMap
        myMap = new HashMap<String, String>();
        myMap.put("1", "1");
        myMap.put("2", "1");
        myMap.put("3", "1");
        myMap.put("4", "1");
        myMap.put("5", "1");
        myMap.put("6", "1");
        System.out.println("HashMap before iterator: " + myMap);
        Iterator<String> it1 = myMap.keySet().iterator();

        while (it1.hasNext()) {
            String key = it1.next();
            if (key.equals("3")) myMap.put(key + "new", "new3");
        }
        System.out.println("HashMap after iterator: " + myMap);
    }

}

运行结果与分析

当我们试着运行上面的程序,输出如下:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
    at java.util.HashMap$KeyIterator.next(HashMap.java:828)
    at com.journaldev.util.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)

查看输出结果,很明显 ConcurrentHashMap 可以支持向 Map 中添加新元素,而 HashMap 则抛出了 ConcurrentModificationException

查看异常堆栈记录,可以发现是下面这条语句抛出了异常:

String key = it1.next();

这就意味着新的元素在 HashMap 中已经插入了,但是在迭代器执行 next() 时出现了错误。事实上,集合对象的迭代器提供了快速失败(Fail-Fast)机制,即修改集合对象结构或者元素数量都会使迭代器触发这个异常。

深入原理:modCount

那么,迭代器是怎么知道 HashMap 被修改了呢?我们可以尝试一次性取出 HashMap 的所有 Key 然后进行遍历。

HashMap 包含一个修改计数器,当你调用它的 next() 方法来获取下一个元素时,迭代器将会用到这个计数器。

HashMap.java

/**
 * HashMap 结构的修改次数
 * 结构修改是指:改变了 HashMap 中 mapping 的个数或者其中的内部结构(比如,重新计算 hash 值)
 * 这个字段在通过 Collection 操作 Hashmap 时提供快速失败(Fail-fast)功能。
 * (参见 ConcurrentModificationException)。
 */
transient volatile int modCount;

特殊情况:提前跳出循环

现在为了证明上面的观点,我们对原来的代码做一点修改,使迭代器在插入新的元素后跳出循环。只要在调用 put 方法后增加一个 break

if (key.equals("3")) {
    myMap.put(key + "new", "new3");
    break;
}

再执行修改后的代码,会得到下面的输出结果:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}
ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}
HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}
HashMap after iterator: {3=1, 2=1, 1=1, 3new=new3, 6=1, 5=1, 4=1}

此时 HashMap 不再抛出异常,因为循环在修改结构后立即终止,迭代器没有机会再次检查 modCount 的变化。

思考:修改值而非结构

最后,如果我们不添加新的元素,而是修改已经存在的键值对,会不会抛出异常呢?

修改原来的程序并且自己验证一下:

// myMap.put(key+"new", "new3");
myMap.put(key, "new3");

如果你对于输出结果感觉困惑或者震惊,欢迎在评论区留言。我会很乐意给出进一步解释。

关于泛型

你有没有注意到我们在创建集合和迭代器时的尖括号?在 Java 中这叫做泛型(Generics),当涉及到编译时的类型检查和去除运行时的 ClassCastException 的时候会很有帮助。点击 这里 可以了解更多泛型教程。

原文链接:journaldev


说明:本文示例代码采用了显式声明泛型类型的写法(如 new HashMap<String, String>()),适用于 Java 7 之前版本。Java 7 及后续版本支持泛型类型推断(钻石操作符 <>),可简化代码书写,但不影响本文所述的核心原理。