怎麼實現讀寫分離(寫時複製)的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


分享到:


相關文章: