1,什么是寫時復制(Copy-On-Write)容器?
寫時復制是指:在並發訪問的情景下,當需要修改JAVA中Containers的元素時,不直接修改該容器,而是先復制一份副本,在副本上進行修改。修改完成之后,將指向原來容器的引用指向新的容器(副本容器)。
2,寫時復制帶來的影響
①由於不會修改原始容器,只修改副本容器。因此,可以對原始容器進行並發地讀。其次,實現了讀操作與寫操作的分離,讀操作發生在原始容器上,寫操作發生在副本容器上。
②數據一致性問題:讀操作的線程可能不會立即讀取到新修改的數據,因為修改操作發生在副本上。但最終修改操作會完成並更新容器,因此這是最終一致性。
3,在JDK中提供了CopyOnWriteArrayList類和CopyOnWriteArraySet類,但是並沒有提供CopyOnWriteMap的實現。因此,可以參考CopyOnWriteArrayList自己實現一個CopyOnWriteHashMap
這里主要是實現 在寫操作時,如何保證線程安全。
import java.util.Collection; import java.util.HashMap; 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>(100);//初始大小應根據實際應用來指定 } @Override public V put(K key, V value) { synchronized (this) { Map<K, V> newMap = new HashMap<K, V>(internalMap);//復制出一個新HashMap V val = newMap.put(key, value);//在新HashMap中執行寫操作 internalMap = newMap;//將原來的Map引用指向新Map return val; } } @Override public void putAll(Map<? extends K, ? extends V> m) { synchronized (this) { Map<K, V> newMap = new HashMap<K, V>(internalMap); newMap.putAll(m); internalMap = newMap; } } @Override public V get(Object key) { V result = internalMap.get(key); return result; } ......//other methods inherit from interface Map
}
從上可以看出,對於put() 和 putAll() 而言,需要加鎖。而讀操作則不需要,如get(Object key)。這樣,當一個線程需要put一個新元素時,它先鎖住當前CopyOnWriteMap對象,並復制一個新HashMap,而其他的讀線程因為不需要加鎖,則可繼續訪問原來的HashMap。
4,應用場景
CopyOnWrite容器適用於讀多寫少的場景。因為寫操作時,需要復制一個容器,造成內存開銷很大,也需要根據實際應用把握初始容器的大小。
不適合於數據的強一致性場合。若要求數據修改之后立即能被讀到,則不能用寫時復制技術。因為它是最終一致性。
總結:寫時復制技術是一種很好的提高並發性的手段。
5,為什么會出現COW?
集合類(ArrayList、HashMap)上的常用操作是:向集合中添加元素、刪除元素、遍歷集合中的元素然后進行某種操作。當多個線程並發地對一個集合對象執行這些操作時就會引發ConcurrentModificationException,比如線程A在for-each中遍歷ArrayList,而線程B同時又在刪除ArrayList中的元素,就可能會拋出ConcurrentModificationException,可以在線程A遍歷ArrayList時加鎖,但由於遍歷操作是一種常見的操作,加鎖之后會影響程序的性能,因此for-each遍歷選擇了不對ArrayList加鎖而是當有多個線程修改ArrayList時拋出ConcurrentModificationException,因此,這是一種設計上的權衡。
為了應對多線程並發修改這種情況,一種策略就是本文的主題“寫時復制”機制;另一種策略是:線程安全的容器類:
ArrayList--->CopyOnWriteArrayList
HashMap--->ConcurrentHashMap
而ConcurrentHashMap並不是從“復制”這個角度來應對多線程並發修改,而是引入了分段鎖(JDK7);CAS、鎖(JDK11)解決多線程並發修改的問題。
參考資料: