1
头图

引言

网络上对于快速失败和安全失败,基本都有这个论断

java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

我对这个持怀疑态度,所以写了代码来测试下

思维导图

image.png

快速失败

定义

在用迭代器遍历一个快速失败集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出 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);
        }
    }

测试结果

image.png
可以看到抛出了ConcurrentModificationException

源码分析

image.png
416行:HashMap源码里面有一个modCount计数器

image.png
显示下所有对于modCount的操作,可以看到只有自增操作,没有自减操作,比较的时候不会产生aba问题

image.png
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");
        }
    }

测试结果

image.png
93行没运行前,list内容和迭代器内容数量均为三个

image.png
93行运行后,lisy的内容为四个,迭代器还是三个

image.png
正常输出结果,且没有输出迭代时增加的“幻想1”

源码分析

image.png
copyOnWriteArrayList的add方法源码如上
440行:拷贝一个数组
441行:对拷贝的数组新增内容
442行:修改copyOnWriteArrayList的数组的引用,指向拷贝出来的数组

image.png
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("程序结束");
    }

测试结果

image.png
这是运行结果,抛出了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自带的一个特殊迭代器

测试结果

image.png
119行没运行前,HashTable和迭代器都是三个值
image.png
119行运行后,HashTable和迭代器都是四个值
image.png
哎,居然能运行,没有抛出ConcurrentModificationException,而且结果居然还输出了四个值

源码分析

image.png
101行:entrySet方法获取set集合
image.png
682行:new了一个EntrySet对象
image.png
688行:EntrySet对象的获取迭代器的方法
image.png
610行:最终返回的是一个Enumerator迭代器对象

所以我举例使用的代码,使用的迭代器对象,本质上都是Enumerator迭代器
image.png
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("程序结束");
    }

测试结果

image.png
这是运行结果,运行成功,且输出了循环中新增进去的值。

源码分析

image.png
70行:entrySet方法获取set集合
image.png
1276行:new了一个EntrySetView对象
image.png
4745行:EntrySetView构造迭代器的方法,把table的引用给迭代器
所以再遍历迭代器时,对concurrentHashMap进行修改,会修改table对象,而迭代器的引用指向的是table,所以迭代器会输出遍历时修改的数据。

结论

所以说java.util.concurrent包下的容器都是安全失败也不对,concurrentHashMap使用迭代器时,并没有复制一个数组进行遍历。


幻想的绝望
1 声望0 粉丝

在绝望中追寻