java:CopyOnWriteArrayList的迭代器支持fast-fail吗 Concurrent包的迭代器支持fast-fail吗

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情 如果你了解这个集合,且清楚写时复制原理,可以直接跳到最后查看答案

什么是CopyOnWriteArrayList

容器分为两大类:Collection和Map 就Map而言,其HashMap在多线程高并发场景下有ConcurrentHashMap来保障线程安全

那么JUC包下有支持list线程安全的容器吗?

首先呢Vector是线程安全的,但它的做法是直接在方法上加synchronized,这导致效率不高。 我们有很多场景是读多写少,写的时候可以加锁保证数据一致性,但是读的时候是没有必要加锁的。 因此CopyOnWriteArrayList和CopyOnWriteArraySet就产生了

它的读写锁的概念和ReentrantReadAndWriteLock的读写锁概念类似,即是读共享锁,写排他锁。但是CopyOnWriteArrayList更进一步的是,它连读都不加锁,并且一边写还能一边读,也就是说写不会阻塞读。这是怎么做到的呢?就要谈到CopyOnWrite写时复制机制,这个机制本身在linux系统底层和其他很多地方都有应用,比如redis的rdb持久化空间时fork子进程就用到了这个机制。

具体是直接把写入的数组拷贝一份,修改操作是在拷贝的副本中进行的,因此在原来对象上的读就不受影响。写完后再指向副本,这样原来的数组就会被GC回收了。并且CopyOnWriteArrayList在写入时的加锁是通过ReentrantLock实现的,即底层是CAS操作,性能上得到了提升。当然在jdk1.8之后synchronized的性能也得到了改善,并不比ReentrantLock差多少,而且官方也更加推荐使用synchronized

什么是fast-fail呢?

即快速失败机制,是java集合中的一种错误检测机制。当使用迭代器Iterator迭代集合的过程中该集合在结构上发生变化时就有可能发生fail-fast,抛出ConcurrentModificationException异常,在不同步的修改并不一定会抛,只是尽量抛出异常

什么叫集合的结构发生变化?就是元素多了或者少了,如果只是修改了元素值结构并没有发生变化

List,HashMap迭代器都支持fast-fail,即迭代器迭代时容器结构发生变化都会抛出ConcurrentModificationException

CopyOnWriteArrayList的迭代器支持fast-fail吗?

不支持! 因为写时复制的存在,CopyOnWriteArrayList在使用迭代器迭代的时候,实际上迭代的是原数组,而不是更新的数组,所以中间如果发生新增、删除元素是在新数组上发生的,所以不影响旧数组的迭代,但是正因为元素的迭代发生在旧数组上,所以迭代时更新的数据是获取不到最新的,存在时延。

测试:

@Test
    public void test3(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
        Iterator<Integer> iterator = list.iterator();
        int i = 0;
        while (iterator.hasNext()){
            if(i == 3){
                list.add(10);
//                list.remove(3);
                list.set(3,111);
            }
            System.out.print(iterator.next()+",");
            i++;
        }

        System.out.println();
        for (Integer v : list) {
            System.out.print(v+",");
        } 
    }
复制代码

输出结果 首先可以看到没有报错,即不支持fail-fast 其次新添元素的和更新的值在迭代时都没有打印出来,因为这时还是用的原数组,更新完成后新数组替换原数组,因此再遍历就能拿到最新的值了 在这里插入图片描述

ConcurrentSkipListMap的迭代器支持fast-fail吗

不支持,实际上ConcurrentHashMap也不支持。并且也都和CopyOnWriteArrayList一样存在着时延。有兴趣可以自己跑跑下面的代码,看看结果,可以再看看其他容器的结果

@Test
    public void test4(){
        ConcurrentSkipListMap<Integer,String> map = new ConcurrentSkipListMap<>();
        for (int i = 0 ; i < 10 ; i++ ) {
            map.put(i,i+"");
        }
        Iterator<Map.Entry<Integer,String>> iterator = map.entrySet().iterator();
        int i = 0 ;
        while(iterator.hasNext()) {
            if (i == 3) {
                // 修改不会影响结构,但是删除,新增会影响结构
//                map.remove(3);
                map.put(3,"111");
                map.put(10,"10");
            }
            Map.Entry<Integer, String> next = iterator.next();
            System.out.println("{key="+next.getKey()+",value="+next.getValue()+"}");
            i++;
        }

        System.out.println("--------");
        for (Integer key : map.keySet()) {
            System.out.println("{key="+key+",value="+map.get(key)+"}");
        }
    }
复制代码

那么这是为什么呢? 实际上util大包下的容器的迭代器都是fast-fail迭代器(强一致性迭代器),而在concurrent小包下的迭代器都是弱一致性迭代器。迭代器使用的不同自然而然就不支持fast-fail了

什么是弱一致性迭代器呢?

所谓强一致性就是修改后立即可见,弱一致性就是修改后不能立即可见。也就是说put操作将一个元素加入后,get在某段时间内是还看不到这个新添的元素的。

查看HashMap的迭代器源码可以看到,每次遍历都会比较当前节点数是否等于原节点数,不等于就报错,它通过这样的操作来保证强一致性。而在ConcurrentHashMap中就没有这步比较了。

// HashMap.java
final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            // 比较当前节点数是否等于原节点数,不等于就报错
            if (modCount != expectedModCount
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
复制代码

在ConcurrentHashMap迭代器中如果已经遍历了的内容发生了变化,则不会抛出错误也不会把变化反馈到迭代器上;但是如果未更新的内容发生了变化,是会反馈到迭代器上的 测试

@Test
    public void test4(){
        ConcurrentSkipListMap<Integer,String> map = new ConcurrentSkipListMap<>();
        ConcurrentHashMap<Integer,String> map2 = new ConcurrentHashMap<>();
        for (int i = 0 ; i < 3 ; i++ ) {
            map.put(i,i+"");
        }
        Iterator<Map.Entry<Integer,String>> iterator = map.entrySet().iterator();
        int i = 0 ;
        while(iterator.hasNext()) {
            if (i == 1) {
                // 修改不会影响结构,但是删除,新增会影响结构
//                map.remove(3);
                map.put(1,"111");
                map.put(2,"111");
//                map.put(10,"10");
            }
            Map.Entry<Integer, String> next = iterator.next();
            System.out.println("{key="+next.getKey()+",value="+next.getValue()+"}");
            i++;
        }

        System.out.println("--------");
        for (Integer key : map.keySet()) {
            System.out.println("{key="+key+",value="+map.get(key)+"}");
        }
    }
复制代码

结果 在这里插入图片描述

为什么concurrent类要用弱一致性迭代器?

1、为了保障多线程并发。我们知道Concurrent是用于多线程并发场景的,如果一个线程在迭代这个容器,另外一个线程就不允许修改它了吗?一修改就报错的话,我们这个多线程并发怎么去保证呢?就多线程而言在乎的是最后的结果,只要迭代完后数据是最新的就足够了,不需要强一致性的迭代器 2、提升了性能。保证了多个线程并发执行的连续性和扩展性。

猜你喜欢

转载自juejin.im/post/7107627403752833038