引言
网络上对于快速失败和安全失败,基本都有这个论断
java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
我对这个持怀疑态度,所以写了代码来测试下
思维导图
快速失败
定义
在用迭代器遍历一个快速失败集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 ConcurrentModificationException
原理
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
例子(HashMap)
测试代码
public static void main(String[] args) {
HashMap<String, Integer> hashMap = new HashMap<>(8);
hashMap.put("幻想4", 4);
hashMap.put("幻想2", 2);
hashMap.put("幻想3", 3);
Set<Map.Entry<String, Integer>> set = hashMap.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
hashMap.put("幻想1", 1);
}
}
测试结果
可以看到抛出了ConcurrentModificationException
源码分析
416行:HashMap源码里面有一个modCount计数器
显示下所有对于modCount的操作,可以看到只有自增操作,没有自减操作,比较的时候不会产生aba问题
1425行:迭代器构造的时候,先用expectedModCount,保存modCount的值
1441行:HashMap提供的迭代器,比较了modCount和expectedModCoun值是否相等,不相等就抛出异常
安全失败
定义
在用迭代器遍历一个安全失败集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),不会抛出 Concurrent Modification Exception,且能修改成功
原理
迭代器遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历
例子(CopyOnWriteArrayList)
测试代码
public static void main(String[] args) {
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
copyOnWriteArrayList.add("幻想4");
copyOnWriteArrayList.add("幻想2");
copyOnWriteArrayList.add("幻想3");
Iterator<String> iterator = copyOnWriteArrayList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
copyOnWriteArrayList.add("幻想1");
}
}
测试结果
93行没运行前,list内容和迭代器内容数量均为三个
93行运行后,lisy的内容为四个,迭代器还是三个
正常输出结果,且没有输出迭代时增加的“幻想1”
源码分析
copyOnWriteArrayList的add方法源码如上
440行:拷贝一个数组
441行:对拷贝的数组新增内容
442行:修改copyOnWriteArrayList的数组的引用,指向拷贝出来的数组
copyOnWriteArrayList的迭代器源码如上
1139行:迭代器构造的时候,就指向了原数组的引用
所以在迭代器内部修改copyOnWriteArrayList,不会对迭代器里的数组产生影响
java.util下的Hashtable原理
测试代码
public static void main(String[] args) {
Hashtable<String, Integer> hashtable = new Hashtable<>(16);
hashtable.put("幻想4", 4);
hashtable.put("幻想2", 2);
hashtable.put("幻想3", 3);
Set<Map.Entry<String, Integer>> set = hashtable.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
hashtable.put("幻想1", 1);
}
System.out.println("程序结束");
}
测试结果
这是运行结果,抛出了ConcurrentModificationException。喔,难道HashTable是快速失败的?别急,兄弟们再看下一个测试代码
测试代码
public static void main(String[] args) {
Hashtable<String, Integer> hashtable = new Hashtable<>(16);
hashtable.put("幻想4", 4);
hashtable.put("幻想2", 2);
hashtable.put("幻想3", 3);
Enumeration<String> keys = hashtable.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
hashtable.put("幻想1", 1);
}
System.out.println("程序结束");
}
对之前的代码修改下迭代器,用了个HashTable自带的一个特殊迭代器
测试结果
119行没运行前,HashTable和迭代器都是三个值
119行运行后,HashTable和迭代器都是四个值
哎,居然能运行,没有抛出ConcurrentModificationException,而且结果居然还输出了四个值
源码分析
101行:entrySet方法获取set集合
682行:new了一个EntrySet对象
688行:EntrySet对象的获取迭代器的方法
610行:最终返回的是一个Enumerator迭代器对象
所以我举例使用的代码,使用的迭代器对象,本质上都是Enumerator迭代器
1381行:hasNext方法,调用了hasMoreElements方法
1388行:next方法,调用了nextElement方法
结论
所以说java.util包下的集合类都是快速失败的是不对,HashTable使用迭代器时,要看调用的是哪个方法。
java.util.concurrent下的concurrentHashMap原理
测试代码
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>(8);
concurrentHashMap.put("幻想4", 4);
concurrentHashMap.put("幻想2", 2);
concurrentHashMap.put("幻想3", 3);
Set<Map.Entry<String, Integer>> set = concurrentHashMap.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
concurrentHashMap.put("幻想1", 1);
}
System.out.println("程序结束");
}
测试结果
这是运行结果,运行成功,且输出了循环中新增进去的值。
源码分析
70行:entrySet方法获取set集合
1276行:new了一个EntrySetView对象
4745行:EntrySetView构造迭代器的方法,把table的引用给迭代器
所以再遍历迭代器时,对concurrentHashMap进行修改,会修改table对象,而迭代器的引用指向的是table,所以迭代器会输出遍历时修改的数据。
结论
所以说java.util.concurrent包下的容器都是安全失败也不对,concurrentHashMap使用迭代器时,并没有复制一个数组进行遍历。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。