HashMap vs ConcurrentHashMap — 示例及Iterator探秘
引言
作为一名 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 及后续版本支持泛型类型推断(钻石操作符 <>),可简化代码书写,但不影响本文所述的核心原理。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/hashmap-vs-concurrenthashmap--shi-li-ji-iterator-tan-mi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。