CopyOnWriteArrayList与Copy On Write写时复制

2022-11-02 15:35:28 浏览数 (1)

前言:

在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,如果每次读取都进行加锁操作,对资源是一种很大的浪费。在不要求数据实时一致性时,我们应该允许多个线程同时访问 List 的内部数据。CopyOnWriteArrayList就实现了这种方式,在它进行读的操作时不会加锁来影响读取效率,而在写的操作时也是加锁后将原数组对象copy出一份来创建一个长度 1的新数组对象,进行对象新增后将引用指向到新数组对象

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


CopyOnWriteArrayList的实现原理:

1. 构造函数

CopyOnWriteArrayList提供了三个构造函数,空参构造,集合参数构造函数,数组对象构造函数,使用时非常方便

代码语言:javascript复制
    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    /**
     * Creates a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection of initially held elements
     * @throws NullPointerException if the specified collection is null
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

    /**
     * Creates a list holding a copy of the given array.
     *
     * @param toCopyIn the array (a copy of this array is used as the
     *        internal array)
     * @throws NullPointerException if the specified array is null
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

2. get方法

无锁的读方法,获取对象时没有任何锁的影响

代码语言:javascript复制
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

3. add方法

以下代码是向ArrayList里添加元素,在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

代码语言:javascript复制
    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 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) {
        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进行加锁


扩展:

基于CopyOnWrite机制扩展实现一个CopyOnWriteMap

代码语言:javascript复制
public class CopyOnWriteHashMap<K,V> implements Map<K,V>, Cloneable{

    private volatile Map<K, V> internalMap;

    final transient ReentrantLock lock = new ReentrantLock();

    public CopyOnWriteHashMap() {
        internalMap = new HashMap<K, V>();
    }

    public V put(K key, V value) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public V remove(Object key) {
        return null;
    }

    public V get(Object key) {
        return internalMap.get(key);
    }

    public void putAll(Map<? extends K, ? extends V> newData) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void clear() {

    }

    @Override
    public Set<K> keySet() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        return null;
    }

    public int size() {
        return internalMap.size();
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean containsKey(Object key) {
        return false;
    }

    @Override
    public boolean containsValue(Object value) {
        return false;
    }
}

Map验证DEMO:

代码语言:javascript复制
public class CopyOnWriteArrayListLab {

    public static void main(String[] args) {
        final Map<Integer, Integer> hashMap = new HashMap<>();// 结果可能大于10000
        final Map<Integer, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());// 结果等于10000
        final Map<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>();// 结果等于10000
        final Map<Integer, Integer> copyOnWriteHashMap = new CopyOnWriteHashMap<>();

        long sendTime = System.currentTimeMillis();
        map(hashMap);
        long endTime = System.currentTimeMillis();
        System.out.println("hashMap耗费时间:" (endTime-sendTime));
        sendTime = System.currentTimeMillis();
        map(syncMap);
        endTime = System.currentTimeMillis();
        System.out.println("syncMap耗费时间:" (endTime-sendTime));
        sendTime = System.currentTimeMillis();
        map(concurrentHashMap);
        endTime = System.currentTimeMillis();
        System.out.println("concurrentHashMap耗费时间:" (endTime-sendTime));
        sendTime = System.currentTimeMillis();
        map(copyOnWriteHashMap);
        endTime = System.currentTimeMillis();
        System.out.println("copyOnWriteHashMap耗费时间:" (endTime-sendTime));
    }

    private static void map(Map<Integer, Integer> map){
        // 往map写入1-10000, key和value相同
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10000; i  ) {
                    map.put(i, i);
                }
            }
        };

        int threadNum = 2;// 线程数
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < threadNum; i  ) {
            Thread thread = new Thread(runnable);
            threadList.add(thread);
            thread.start();
        }

        // 主线程等待子线程执行完成
        for (Thread thread : threadList) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("map数量" map.size());
    }

}

对比一下HashMap、Collections.synchronizedMap(new HashMap<>())、ConcurrentHashMap、CopyOnWriteHashMap几种Map对象的写入效率和线程安全情况

执行结果:

对象

是否线程安全

耗时(ms)/w

写入效率

HashMap

7

较高

Collections.synchronizedMap

3

较高

ConcurrentHashMap

8

较高

CopyOnWriteHashMap

2423

低(每次写入新建一次对象)

  经过十几次的执行测试除了CopyOnWriteHashMap其余map的写入时间大概都在10毫秒左右相差不大,而CopyOnWriteHashMap因为每次写入都需要copy一份旧对象写入效率低下并且占用内存,在写入较多的情况下不建议使用

List验证DEMO:

在同一时间,多个线程对ArrayList进行新增或删除时就会抛出并发失败异常(java.util.ConcurrentModificationException),当我们使用CopyOnWriteArrayList        时在进行写读操作时则正常

代码语言:javascript复制
public class CopyOnWriteArrayListLab {

    private static final Integer THREAD_POOL_MAX_SIZE = 10;

    // 不支持并发
    //private static List<String> mList = new ArrayList<>();
    // 支持并发
    private static List<String> mList = new CopyOnWriteArrayList<>();

    /**
     * List读写不支持并发测试
     */
    public static void main(String[] args) {
        startTest();
    }

    /**
     * 初始化数据
     */
    private static void initList() {
        for (int i = 0; i < 10; i  ) {
            mList.add("line:"   (i   1)   "data");
        }
    }

    private static void startTest() {
        // 初始化数据
        initList();
        System.out.println("------------初始化完成--------------------------");
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_MAX_SIZE);

        // 读写并发测试
        for (int i = 0; i < THREAD_POOL_MAX_SIZE; i  ) {
            // 读任务立即执行
            executorService.execute(() -> {
                for (String item : mList) {
                    System.out.println(Thread.currentThread().getName()   "读数据:"   item);
                }
            });
            final int final1 = i   10;
            // 写任务立即执行
            executorService.execute(() -> {
                mList.add("写线程添加数据"   final1   "..............");
            });
        }
    }

}

应用场景:

CopyOnWrite用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景。像类似这种内容更新不频繁的可以使用CopyOnWrite,每天晚上定时更新,大部分为读取操作。

缺点:

内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。

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

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性,如果对读取的实时数据有要求还是避免使用CopyOnWrite。

黑名单场景DEMO:

假设有一个业务为每次需要校验用户或者商品等等是不是在黑名单内,而黑名单每天定时更新的场景下我们使用CopyOnWriteHashMap来进行这种读多写少的情况,map的K,V结构可以快速判断id是否存在于黑名单中,而在读取的时候不受锁的影响,对业务响应时间影响最小

代码语言:javascript复制
public class CowMapBlackList {

    private static CopyOnWriteHashMap<Integer, Boolean> blackListMap = new CopyOnWriteHashMap<>();

    public static boolean isBlackList(Integer id) {
        return blackListMap.get(id) == null ? false : true;
    }

    public static void addBlackList(Integer id) {
        blackListMap.put(id, Boolean.TRUE);
    }

    /**
     * 批量添加黑名单
     *
     * @param ids
     */
    public static void addBlackList(Map<Integer,Boolean> ids) {
        blackListMap.putAll(ids);
    }

    public static void main(String[] args) {
        Map<Integer, Boolean> blackListMap = new HashMap<>();
        for (int i = 0; i < 100; i  ) {
            blackListMap.put(i,Boolean.TRUE);
        }
        addBlackList(blackListMap);
        System.out.println(isBlackList(111));
    }
}

0 人点赞