• 第十六章、CopyOnWrite容器


    16.1 什么是CopyOnWrite容器

      在说到CopyOnWrite容器之前我们先来谈谈什么是CopyOnWrite机制,CopyOnWrite是计算机设计领域中的一种优化策略,也是一种在并发场景下常用的设计思想——写入时复制思想。

      那什么是写入时复制思想呢?就是当有多个调用者同时去请求一个资源数据的时候,有一个调用者出于某些原因需要对当前的数据源进行修改,这个时候系统将会复制一个当前数据源的副本给调用者修改。

      CopyOnWrite容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。

      这样做的好处在于,我们可以在并发的场景下对容器进行读操作而不需要加锁,从而达到读写分离的目的。从JDK 1.5 开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器 ,分别是CopyOnWriteArrayList和CopyOnWriteArraySet 。我们着重给大家介绍一下CopyOnWriteArrayList。

    16.2 CopyOnWriteArrayList

    • 优点: CopyOnWriteArrayList经常被用于“读多写少”的并发场景,是因为CopyOnWriteArrayList无需任何同步措施,大大增强了读的性能。在Java中遍历线程非安全的List(如:ArrayList和 LinkedList)的时候,若中途有别的线程对List容器进行修改,那么会抛出ConcurrentModificationException异常。CopyOnWriteArrayList由于其读写分离,遍历和修改操作分别作用在不同的List容器,所以在使用迭代器遍历的时候,则不会抛出异常。
    • 缺点: 第一个缺点是CopyOnWriteArrayList每次执行写操作都会将原容器进行拷贝了一份,数据量大的时候,内存会存在较大的压力,可能会引起频繁Full GC(ZGC因为没有使用Full GC)。比如这些对象占用的内存比较大200M左右,那么再写入100M数据进去,内存就会多占用300M。

      第二个缺点是CopyOnWriteArrayList由于实现的原因,写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据。

      现在我们来看一下CopyOnWriteArrayList的add操作源码,它的逻辑很清晰,就是先把原容器进行copy,然后在新的副本上进行“写操作”,最后再切换引用,在此过程中是加了锁的。

    public boolean add(E e) {
    
        // ReentrantLock加锁,保证线程安全
        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();
        }
    }

      我们再来看一下remove操作的源码,remove的逻辑是将要remove元素之外的其他元素拷贝到新的副本中,然后切换引用,再将原容器的引用指向新的副本中,因为remove操作也是“写操作”所以也是要加锁的。

    public E remove(int index) {
    
            // 加锁
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] elements = getArray();
                int len = elements.length;
                E oldValue = get(elements, index);
                int numMoved = len - index - 1;
                if (numMoved == 0)
                    // 如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
                    setArray(Arrays.copyOf(elements, len - 1));
                else {
                    // 否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
                    Object[] newElements = new Object[len - 1];
                    System.arraycopy(elements, 0, newElements, 0, index);
                    System.arraycopy(elements, index + 1, newElements, index,
                                     numMoved);
                    setArray(newElements);
                }
                return oldValue;
            } finally {
                // 解锁
                lock.unlock();
            }
        }

      我们再来看看CopyOnWriteArrayList效率最高的读操作的源码

    public E get(int index) {
        return get(getArray(), index);
    }
     private E get(Object[] a, int index) {
         return (E) a[index];
     }

      由上可见“读操作”是没有加锁,直接读取。

    16.3 CopyOnWrite的业务中实现

      接下来,我们来具体结合业务场景实现一个CopyOnWriteMap的并发容器并且来使用它。

    import java.util.Collection;
    import java.util.Map;
    import java.util.Set;
    
    public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
        private volatile Map<K, V> internalMap;
    
        public CopyOnWriteMap() {
            internalMap = new HashMap<K, V>();
        }
    
        public V put(K key, V value) {
            synchronized (this) {
                Map<K, V> newMap = new HashMap<K, V>(internalMap);
                V val = newMap.put(key, value);
                internalMap = newMap;
                return val;
            }
        }
    
        public V get(Object key) {
            return internalMap.get(key);
        }
    
        public void putAll(Map<? extends K, ? extends V> newData) {
            synchronized (this) {
                Map<K, V> newMap = new HashMap<K, V>(internalMap);
                newMap.putAll(newData);
                internalMap = newMap;
            }
        }
    } 

      上面就是参考CopyOnWriteArrayList实现的CopyOnWriteMap,我们可以用这个容器来做什么呢?结合我们之前说的CopyOnWrite的复制思想,它最适用于“读多写少”的并发场景。

      场景:假如我们有一个搜索的网站需要屏蔽一些“关键字”,“黑名单”每晚定时更新,每当用户搜索的时候,“黑名单”中的关键字不会出现在搜索结果当中,并且提示用户敏感字。

     

    // 黑名单服务
    public class BlackListServiceImpl {
        // 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
        private static CopyOnWriteMap<String, Boolean> blackListMap = 
            new CopyOnWriteMap<String, Boolean>(1000);
    
        public static boolean isBlackList(String id) {
            return blackListMap.get(id) == null ? false : true;
        }
    
        public static void addBlackList(String id) {
            blackListMap.put(id, Boolean.TRUE);
        }
    
        /**
         * 批量添加黑名单
         * (使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
         * 如使用上面代码里的addBlackList方法)
         * @param ids
         */
        public static void addBlackList(Map<String,Boolean> ids) {
            blackListMap.putAll(ids);
        }
    
    }

      这里需要各位小伙伴特别特别注意一个问题,此处的场景是每晚凌晨“黑名单”定时更新,原因是CopyOnWrite容器有数据一致性的问题,它只能保证最终数据一致性

      所以如果我们希望写入的数据马上能准确地读取,请不要使用CopyOnWrite容器。

    参考资料

  • 相关阅读:
    About_Web
    神奇的 SQL 之性能优化 → 让 SQL 飞起来
    Java实现Kafka生产者和消费者的示例
    Android屏幕绘制一问到底(无代码)
    关于数据库事务和锁的必会知识点,你掌握了多少?
    【Azure Cloud Services】云服务频繁发生服务器崩溃的排查方案
    Choreographer全解析
    气之争,聊聊算法岗位的门户之见!
    资深首席架构师预测:2021年云计算的8个首要趋势
    【并发编程】- 内存模型(针对JSR-133内存模型)篇
  • 原文地址:https://www.cnblogs.com/jdy1022/p/14370880.html
Copyright © 2020-2023  润新知