美文网首页
CopyOnWriteArrayList分析

CopyOnWriteArrayList分析

作者: 胖瘦馒头 | 来源:发表于2018-07-16 21:08 被阅读0次

    参考文章:http://ifeve.com/java-copy-on-write/

    Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

    什么是CopyOnWrite容器

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    CopyOnWriteArrayList的实现原理(JDK9源码)

    • 增加元素

    每次增加元素的时候,都会先对此对象进行锁持有操作,然后进行新创建一个全新的数组,将添加的数组放在尾部,再将原数组的引用指向新数组。

    /**
        * Appends the specified element to the end of this list.
        *
        * @param e element to be appended to this list
        * @return {@code true} (as specified by {@link Collection#add})
        */
       public boolean add(E e) {
           synchronized (lock) {
               Object[] elements = getArray();
               int len = elements.length;
               Object[] newElements = Arrays.copyOf(elements, len + 1); //新创建一个新数组,大小比老数组多一
               newElements[len] = e; //把新元素放入新数组之中
               setArray(newElements); //把原数组引用指向新数组
               return true;
           }
       }
    
    • 删除元素

    删除元素与添加元素大体实现方式相同,也是会先对此对象进行锁持有操作,然后进行新创建一个小一位的数组,把删除元素之后的剩下的元素复制到此数组之中,然后将老数组的引用指向新数组。

     /**
         * A version of remove(Object) using the strong hint that given
         * recent snapshot contains o at the given index.
         */
        private boolean remove(Object o, Object[] snapshot, int index) {
            synchronized (lock) {
                Object[] current = getArray();
                int len = current.length;
                if (snapshot != current) findIndex: {
                    int prefix = Math.min(index, len);
                    for (int i = 0; i < prefix; i++) {
                        if (current[i] != snapshot[i]
                            && Objects.equals(o, current[i])) {
                            index = i;
                            break findIndex;
                        }
                    }
                    if (index >= len)
                        return false;
                    if (current[index] == o)
                        break findIndex;
                    index = indexOf(o, current, index, len);
                    if (index < 0)
                        return false;
                }
                Object[] newElements = new Object[len - 1];
                System.arraycopy(current, 0, newElements, 0, index);
                System.arraycopy(current, index + 1,
                                 newElements, index,
                                 len - index - 1);
                setArray(newElements);
                return true;
            }
        }
    
    • 读取元素

    因为存储元素的数组被volatile修饰的,所以保证了其元素的可见性,即在每次读取元素的时候,都是最新值。但是如果在数组进行增加或者删除的中间进行读取,则读取到的数值就是老数值了,这也就是CopyOnWrite的一个缺点:只能保证数据的最终一致性,但是不能保证数据的实时一致性。

       
        /** The array, accessed only via getArray/setArray. */
       private transient volatile Object[] array;
    
      /**
        * {@inheritDoc}
        *
        * @throws IndexOutOfBoundsException {@inheritDoc}
        */
       public E get(int index) {
           return elementAt(getArray(), index);
       }
    

    CopyOnWrite的缺点

    • 内存占用问题。

    因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

    • 数据一致性问题

      CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

    相关文章

      网友评论

          本文标题:CopyOnWriteArrayList分析

          本文链接:https://www.haomeiwen.com/subject/oekipftx.html