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

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

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

  • 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 个副本出来。this

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容器

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

相关文章
相关标签/搜索