• JUC源码分析-集合篇(四)CopyOnWriteArrayList


    JUC源码分析-集合篇(四)CopyOnWriteArrayList

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

    • CopyOnWriteArrayList ArrayList 线程安全的实现
    • CopyOnWriteArraySetSet 线程安全的实现

    1. CopyOnWrite 容器

    1.1 什么是 CopyOnWrite 容器

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

    1.2 CopyOnWrite 应用场景

    CopyOnWrite 并发容器用于读多写少的并发场景。使用 CopyOnWriteMap 需要注意两件事情:

    1. 减少扩容开销。根据实际需要,初始化 CopyOnWriteMap 的大小,避免写时 CopyOnWriteMap 扩容的开销。

    2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。

    1.3 CopyOnWrite 缺点

    CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

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

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

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

    2. CopyOnWriteArrayList 实现原理

    List<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    list.get(0);
    

    在使用 CopyOnWriteArrayList 之前,我们先阅读其源码了解下它是如何实现的。以下代码是向 ArrayList 里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会 Copy 出 N 个副本出来。

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    

    读的时候不需要加锁,如果读的时候有多个线程正在向 ArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的 ArrayList。

    public E get(int index) {
        return get(getArray(), index);
    }
    

    实现很简单,只要了解了 CopyOnWrite 机制,我们可以实现各种 CopyOnWrite 容器,并且在不同的应用场景中使用。

    3. CopyOnWriteArraySet 实现原理

    CopyOnWriteArraySet 底层全部使用 CopyOnWriteArrayList 实现。

    public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
    
    // 增删改查都是调用 CopyOnWriteArrayList 的方法
    public boolean add(E e) { return al.addIfAbsent(e); }
    public boolean remove(Object o) { return al.remove(o); }
    public boolean contains(Object o) { return al.contains(o); }
    

    以 addIfAbsent 为例

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        // indexOf 查找指定元素e在snapshot数组中的索引位置
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }
    
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    

    参考:

    1. 聊聊并发-Java中的Copy-On-Write容器

    每天用心记录一点点。内容也许不重要,但习惯很重要!

  • 相关阅读:
    将"089,0760,009"变为 89,760,9
    单向循环链表
    双链表复习
    【C语言】scanf()输入浮点型数据
    【C语言】一元二次方程(求实根和虚根)
    输入一个三位正整数,输出百位数,十位数,个位数
    输入身份证号,输出出生日期
    比较四个数的大小
    比较三个数的大小
    比较两个数的大小
  • 原文地址:https://www.cnblogs.com/binarylei/p/10923764.html
Copyright © 2020-2023  润新知