怎么实现读写分离(写时复制)的Map?


怎么实现读写分离(写时复制)的Map?

为什么还需要使用读写分离的Map?


首先我们先介绍一下常用的Map

  1. 线程不安全
    • HashMap : 非线程安全,即任一时刻可以由多个线程同时写HashMap,可能会导致数据不一致。
    • LinkedListHashMap: 非线程安全,保证 加入的顺序是怎么,循环出来就是什么顺序。可以理解为维护了元素次序的HashMap。
    • TreeMap:有序的集合,可以实现自定义排序。

2.线程安全

    • HashTable : 线程安全,但是读和写都需要加锁,性能比较低。
    • ConcurrentHashMap: 采用分段锁实现,但是同一段的写和读也是互斥的,所以性能稍微低

那么如何实现一个缓存呢,可以进行并发的读和写的,从上面介绍类看,

无论是HashTable 还是 ConcurrentHashMap 都会遇到读加锁的情况,所以不太适合这种场景。

那么我们下面实现可以进行读写分离的Map,即写加锁,读不加锁。

CopyOnWriteMap 的实现

1、实现步骤如下

  1. 定义一个由初始容量的map.并使用volatile 修饰,这个为了保证在多线程环境下可见性。
  2. 定义一个 可重入锁 ReentrantLock
  3. 定义 put 方法,putAll方法,get方法

2、代码实现

怎么实现读写分离(写时复制)的Map?

3、说明

  • put 方法
    • 首先加锁
    • new 一个新的Map,然后把旧的map放进去
    • 在新的Map中添加元素
    • 将旧map 的引用指向 新的 map
    • 释放锁
  • get 方法
    • get 方法不需要获取锁,直接获取即可。

使用场景


假如有下面一个场景,一个比较小的电商网站(商品信息比较少),所以我们可以直接缓存在内存中,

后期添加、删除、更新都会更新内存中的缓存。

实现代码如下:


怎么实现读写分离(写时复制)的Map?

使用CopyOnWriteMap需要的注意事项:

1、尽量批量添加,否则每次添加都会复制一个map容器,可能会造成频繁的GC(垃圾回收)。

2、尽量初始化时指定容量

CopyOnWriteMap 优缺点

  1. 优点
    1. 实现读写分离,读不加锁,写加锁,性能比较高,适合在高并发场景下使用。
    2. 对比HashTable 和 ConcurrentHashMap ,CopyOnWriteMap 优势在于读不加锁。并发高。
  2. 缺点
    1. 内存占用高的问题。
    2. 对写入的数据不能及时读出来

下面说明为什么内存占用高:

<code>\tpublic V put(K key, V value) {
\t\ttry {
\t\t\tlock.lock();
\t\t\tMap newMap = new HashMap<>(map);
\t\t\tV v = newMap.put(key, value);
\t\t\tmap = newMap;
\t\t\treturn v;
\t\t} catch (Exception e) {
\t\t}finally {
\t\t\tlock.unlock();
\t\t}
\t\treturn null;
\t}
/<code>


从上面看,put操作首先 创建一个新的map,然后把添加的元素添加到新map中,

最后把旧的 map引用指向新的map内存地址。

示意图如下:


怎么实现读写分离(写时复制)的Map?

执行 map = newMap; 代码后:


怎么实现读写分离(写时复制)的Map?

上面旧的map引用虽然指向了新的Map,但是旧的Map如果回收不及时,会造成内存溢出。

为什么要使用COW ?


怎么实现读写分离(写时复制)的Map?

java util 下的集合类都不能应付并发的修改和添加,比如在迭代集合的时候对此集合进行删除,

则会出现 ConcurrentModificationException异常,或者 多个线程并发的添加,

会出现 上一个添加的值被下一个值覆盖。

为了应付并发的修改,有以下两种办法

1、写时复制

2、线程安全的容器

比如: ArrayList --> CopyOnWriteList

HashSet --> CopyOnWriteSet

HashMap --> ConcurrentHashMap


分享到:


相關文章: