关于java:看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap

48次阅读

共计 5206 个字符,预计需要花费 14 分钟才能阅读完成。

引言

面试官: 小伙子你有点眼生啊,是不是去年来这面试过啊。<br/>
二胖: 啊,没有啊我这是第一次来这。<br/>
面试官: 行,那咱们开始明天的面试吧,刚开始咱们先来点简略的吧,java外面的容器你晓得哪些啊,跟我说一说吧。<br/>
二胖: 好的,java 外面常见容器有 ArrayList(线程非平安)、HashMap(线程非平安)、HashSet(线程非平安),ConcurrentHashMap(线程平安)。<br/>
面试官: ArrayList 既然线程非平安那有没有线程平安的 ArrayList 列?<br/>
二胖: 这个。。。如同问到常识盲点了。<br/>
面试官: 那咱们明天的面试就先到这了,我待会还有一个会,后续如有告诉人事会分割你的。<br/>
以上故事纯属虚构如有雷同请以本文为主。

什么是 COW

在 java 外面说到汇合容器咱们个别首先会想到的是 HashMapArrayListHasHSet 这几个容器也是平时开发中用的最多的。
这几个都是非线程平安的,如果咱们有特定业务须要应用线程的平安容器列,

  • HashMap能够用 ConcurrentHashMap 代替。
  • ArrayList 能够应用 Collections.synchronizedList() 办法 (list 每个办法都用synchronized 润饰) 或者应用 Vector(当初根本也不必了,每个办法都用synchronized 润饰)

或者应用CopyOnWriteArrayList 代替。

  • HasHSet 能够应用 Collections.synchronizedSet 或者应用 CopyOnWriteArraySet 来代替。(CopyOnWriteArraySet 为什么不叫 CopyOnWriteHashSet 因为 CopyOnWriteArraySet 底层是采纳 CopyOnWriteArrayList 来实现的)

咱们能够看到 CopyOnWriteArrayList 在线程平安的容器外面屡次呈现。
首先咱们来看看什么是 CopyOnWriteCopy-On-Write 简称COW,是一种用于程序设计中的优化策略。

CopyOnWrite 容器即写时复制的容器。艰深的了解是当咱们往一个容器增加元素的时候,不间接往以后容器增加,而是先将以后容器进行 Copy,复制出一个新的容器,而后新的容器里增加元素,增加完元素之后,再将原容器的援用指向新的容器。这样做的益处是咱们能够对 CopyOnWrite 容器进行并发的读,而不须要加锁,因为以后容器不会增加任何元素。所以 CopyOnWrite 容器也是一种读写拆散的思维,读和写不同的容器。

为什么要引入 COW

避免 ConcurrentModificationException 异样

在 java 外面咱们如果采纳不正确的循环姿态去遍历 List 时候,如果一边遍历一边批改抛出 java.util.ConcurrentModificationException 谬误的。
如果对 ArrayList 循环遍历不是很相熟的能够倡议看下这篇文章《ArrayList 的删除姿态你都把握了吗》

        List<String> list = new ArrayList<>();
        list.add("张三");
        list.add("java 金融");
        list.add("javajr.cn");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){String content = iterator.next();
            if("张三".equals(content)) {list.remove(content);
            }

        }

下面这个栗子是会产生 java.util.ConcurrentModificationException 异样的,如果把 ArrayList 改为CopyOnWriteArrayList 是不会产生生异样的。

线程平安的容器

咱们再看上面一个栗子一个线程往 List 外面增加数据,一个线程循环 list 读数据。

    List<String> list = new ArrayList<>();
        list.add("张三");
        list.add("java 金融");
        list.add("javajr.cn");
        Thread t = new Thread(new Runnable() {
            int count = 0;
            @Override
            public void run() {while (true) {list.add(count++ + "");
                }
            }
        });
        t.start();
        Thread.sleep(10000);
        for (String s : list) {System.out.println(s);
        }

咱们运行上述代码也会产生 ConcurrentModificationException 异样,如果把 ArrayList 换成了 CopyOnWriteArrayList 就一切正常。

CopyOnWriteArrayList 的实现

通过下面两个栗子咱们能够发现 CopyOnWriteArrayList 是线程平安的,上面咱们就来一起看看 CopyOnWriteArrayList 是如何实现线程平安的。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

从源码中咱们能够晓得 CopyOnWriteArrayList 这和 ArrayList 底层实现都是通过一个 Object 的数组来实现的,只不过 CopyOnWriteArrayList的数组是通过 volatile 来润饰的,为什么须要 volatile 润饰倡议能够看看《Java 的 synchronized 能避免指令重排序吗?》
还有新增了ReentrantLock

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();}
    }

上述源码咱们能够发现比较简单,有几个点须要略微留神下

  • 减少数据的时候是通过 ReentrantLock 加锁操作来(在 jdk11 的时候采纳了 synchronized 来替换 ReentrantLock) 保障多线程写的时候只有一个线程进行数组的复制,否则的话内存中会有多份被复制的数据,导致数据错乱。
  • 数组是通过volatile 润饰的,依据 volatilehappens-before 规定,写线程对数组援用的批改是能够立刻对读线程是可见的。
  • 通过写时复制来保障读写切实两个不同的数据容器中进行操作。

本人实现一个 COW 容器

再 Java 并发包里提供了两个应用 CopyOnWrite 机制实现的并发容器, 它们是 CopyOnWriteArrayListCopyOnWriteArraySet,然而并没有 CopyOnWriteHashMap 咱们能够依照他的思路本人来实现一个CopyOnWriteHashMap

public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable {final transient ReentrantLock lock = new ReentrantLock();

    private volatile Map<K, V> map;


    public CopyOnWriteHashMap() {map = new HashMap<>();
    }

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

    @Override
    public V get(Object key) {return map.get(key);
    }
    @Override
    public V remove(Object key) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {Map<K, V> newMap = new HashMap<K, V>(map);

            if (!newMap.containsKey(key)) {return null;}
            V v = newMap.get(key);
            newMap.remove(key);
            map = newMap;
            return v;
        }finally {lock.unlock();
        }
    }

上述咱们实现了一个简略的 CopyOnWriteHashMap,只实现了add、remove、get 办法其余残余的办法能够自行去实现,波及到只有数据变动的就要加锁,读无需加锁。

利用场景

CopyOnWrite并发容器实用于读多写少的并发场景,比方黑白名单、国家城市等根底数据缓存、系统配置等。这些根本都是只有想我的项目启动的时候初始化一次,变更频率十分的低。如果这种读多写少的场景采纳 Vector,Collections包装的这些形式是不合理的,因为只管多个读线程从同一个数据容器中读取数据,然而读线程对数据容器的数据并不会产生产生批改,所以并不需要读也加锁。

CopyOnWrite 毛病

CopyOnWriteArrayList 尽管是一个线程平安版的 ArrayList,但其每次批改数据时都会复制一份数据进去,所以 CopyOnWriteArrayList 只实用读多写少或无锁读场景。咱们如果在理论业务中应用 CopyOnWriteArrayList,肯定是因为这个场景适宜而非是为了炫技。

内存占用问题

因为 CopyOnWrite 的写时复制机制每次进行写操作的时候都会有两个数组对象的内存,如果这个数组对象占用的内存较大的话,如果频繁的进行写入就会造成频繁的 Yong GC 和 Full GC。

数据一致性问题

CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。读操作的线程可能不会立刻读取到新批改的数据,因为批改操作产生在正本上。但最终批改操作会实现并更新容器所以这是最终一致性。

CopyOnWriteArrayList 和 Collections.synchronizedList()

简略的测试了下 CopyOnWriteArrayList 和 Collections.synchronizedList()的读和写发现:

  • 在高并发的写时 CopyOnWriteArray 比同步 Collections.synchronizedList 慢百倍
  • 在高并发的读性能时 CopyOnWriteArray 比同步 Collections.synchronizedList 快几十倍。
  • 高并发写时,CopyOnWriteArrayList 为何这么慢呢?因为其每次 add 时,都用 Arrays.copyOf 创立新数组,频繁 add 时内存申请开释性能耗费大。
  • 高并发读的时候 CopyOnWriteArray 无锁,Collections.synchronizedList 有锁所以读的效率比拟低下。

总结

抉择 CopyOnWriteArrayList 的时候肯定是读远大于写。如果读写都差不多的话倡议抉择 Collections.synchronizedList。

完结

  • 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来, 我会对其加以修改。
  • 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
  • 感谢您的浏览, 非常欢送并感谢您的关注。


伟人肩膀摘苹果
http://ifeve.com/java-copy-on…

正文完
 0