2017年8月3日 星期四

[ Java 文章收集 ] 聊聊並發 - Java 中的 Copy-On-Write 容器

Source From Here 
Preface 
Copy-On-Write 簡稱 COW,是一種用於程序設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容 Copy 出去形成一個新的內容然後再改,這是一種延時懶惰策略。從 JDK1.5 開始 Java 並發包裡提供了兩個使用 CopyOnWrite 機制實現的並發容器,它們是 CopyOnWriteArrayList 和 CopyOnWriteArraySet。CopyOnWrite 容器非常有用,可以在非常多的並發場景中使用到。 

什麼是 CopyOnWrite 容器 
CopyOnWrite 容器即寫時復制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite 容器進行並發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以 CopyOnWrite 容器也是一種讀寫分離的思想,讀和寫不同的容器。 

CopyOnWriteArrayList 的實現原理 
在使用 CopyOnWriteArrayList 之前,我們先閱讀其源碼了解下它是如何實現的。以下代碼是向 ArrayList 裡添加元素,可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。 
  1. public boolean add(T e) {  
  2.     final ReentrantLock lock = this.lock;  
  3.     lock.lock();  
  4.     try {  
  5.         Object[] elements = getArray();  
  6.         int len = elements.length;  
  7.         // 复制出新数组  
  8.         Object[] newElements = Arrays.copyOf(elements, len + 1);  
  9.         // 把新元素添加到新数组里  
  10.         newElements[len] = e;  
  11.         // 把原数组引用指向新数组  
  12.         setArray(newElements);  
  13.         return true;  
  14.   
  15.     } finally {  
  16.         lock.unlock();  
  17.     }  
  18. }  
  19.   
  20. final void setArray(Object[] a) {  
  21.     array = a;  
  22. }  
讀的時候不需要加鎖,如果讀的時候有多個線程正在向 ArrayList 添加數據,讀還是會讀到舊的數據,因為寫的時候不會鎖住舊的 ArrayList: 
  1. public E get(int index) {  
  2.     return get(getArray(), index);  
  3. }  
JDK中並沒有提供 CopyOnWriteMap,我們可以參考 CopyOnWriteArrayList 來實現一個,基本代碼如下: 
  1. import java.util.Collection;  
  2. import java.util.Map;  
  3. import java.util.Set;  
  4.   
  5. public class CopyOnWriteMap implements Map, Cloneable {  
  6.     private volatile Map internalMap;  
  7.   
  8.     public CopyOnWriteMap() {  
  9.         internalMap = new HashMap();  
  10.     }  
  11.   
  12.     public V put(K key, V value) {  
  13.   
  14.         synchronized (this) {  
  15.             Map newMap = new HashMap(internalMap);  
  16.             V val = newMap.put(key, value);  
  17.             internalMap = newMap;  
  18.             return val;  
  19.         }  
  20.     }  
  21.   
  22.     public V get(Object key) {  
  23.         return internalMap.get(key);  
  24.     }  
  25.   
  26.     public void putAll(Mapextends K, ? extends V> newData) {  
  27.         synchronized (this) {  
  28.             Map newMap = new HashMap(internalMap);  
  29.             newMap.putAll(newData);  
  30.             internalMap = newMap;  
  31.         }  
  32.     }  
  33. }  
實現很簡單,只要了解了 CopyOnWrite 機制,我們可以實現各種 CopyOnWrite 容器,並且在不同的應用場景中使用。 

CopyOnWrite 的應用場景 
CopyOnWrite 並發容器用於讀多寫少的並發場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜索。實現代碼如下: 
  1. package com.ifeve.book;  
  2.   
  3. import java.util.Map;  
  4.   
  5. import com.ifeve.book.forkjoin.CopyOnWriteMap;  
  6.   
  7. /** 
  8. * 黑名单服务 
  9. * 
  10. * @author fangtengfei 
  11. * 
  12. */  
  13. public class BlackListServiceImpl {  
  14.   
  15.     private static CopyOnWriteMap blackListMap = new CopyOnWriteMap(  
  16.             1000);  
  17.   
  18.     public static boolean isBlackList(String id) {  
  19.         return blackListMap.get(id) == null ? false : true;  
  20.     }  
  21.   
  22.     public static void addBlackList(String id) {  
  23.         blackListMap.put(id, Boolean.TRUE);  
  24.     }  
  25.   
  26.     /** 
  27.      * 批量添加黑名单 
  28.      * 
  29.      * @param ids 
  30.      */  
  31.     public static void addBlackList(Map ids) {  
  32.         blackListMap.putAll(ids);  
  33.     }  
  34.   
  35. }  
代碼很簡單,但是使用 CopyOnWriteMap 需要注意兩件事情: 
1. 減少擴容開銷。根據實際需要,初始化 CopyOnWriteMap 的大小,避免寫時 CopyOnWriteMap 擴容的開銷。
2. 使用批量添加。因為每次添加,容器每次都會進行複制,所以減少添加次數,可以減少容器的複制次數。如使用上面代碼裡的 addBlackList 方法。

CopyOnWrite 的缺點 
CopyOnWrite 容器有很多優點,但是同時也存在兩個問題,即內存佔用問題和數據一致性問題。所以在開發的時候需要注意一下。 

內存佔用問題 
因為 CopyOnWrite 的寫時復制機制,所以在進行寫操作的時候,內存裡會同時駐紮兩個對象的內存,舊的對象和新寫入的對象注意:在復制的時候只是複制容器裡的引用,只是在寫的時候會創建新對象添加到新容器裡,而舊容器的對像還在使用,所以有兩份對象內存)。如果這些對象佔用的內存比較大,比如說200M左右,那麼再寫入100M數據進去,內存就會佔用300M,那麼這個時候很有可能造成頻繁的 Yong GC 和 Full GC。之前我們系統中使用了一個服務由於每晚使用 CopyOnWrite 機制更新大對象,造成了每晚 15 秒的 Full GC,應用響應時間也隨之變長。 

針對內存佔用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是 10 進制的數字,可以考慮把它壓縮成 36 進製或 64 進制。或者不使用 CopyOnWrite 容器,而使用其他的並發容器,如 ConcurrentHashMap。 

數據一致性問題 
CopyOnWrite 容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用 CopyOnWrite 容器。

沒有留言:

張貼留言

[ FP In Python ] Ch1. (Avoiding) Flow Control

Preface   In typical imperative Python programs—including those that make use of classes and methods to hold their imperative code—a block...