JAVA中寫時復制(Copy-On-Write)Map實現


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)解決多線程並發修改的問題。

 

參考資料:

聊聊並發-Java中的Copy-On-Write容器

Java 集合系列10之 HashMap詳細介紹(源碼解析)和使用示例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM