看了CopyOnWriteArrayList後自己實現了一個CopyOnWriteHashMap

Python實戰社羣

Java實戰社羣

長按識別下方二維碼,按需求添加

掃碼關注添加客服

進Python社羣▲

掃碼關注添加客服

進Java社羣

作者丨碼農二胖

來源丨java金融



引言

面試官: 小夥子你有點眼熟啊,是不是去年來這面試過啊。
二胖: 啊,沒有啊我這是第一次來這。
面試官: 行,那我們開始今天的面試吧,剛開始我們先來點簡單的吧,java裏面的容器你知道哪些啊,跟我說一說吧。
二胖: 好的,java裏面常見容器有ArrayList(線程非安全)、HashMap(線程非安全)、HashSet(線程非安全),ConcurrentHashMap(線程安全)。
面試官: ArrayList 既然線程非安全那有沒有線程安全的ArrayList列?
二胖:  這個。。。好像問到知識盲點了。
面試官:  那我們今天的面試就先到這了,我待會還有一個會,後續如有通知人事會聯繫你的。
以上故事純屬虛構如有雷同請以本文爲主。






什麼是COW

在java裏面說到集合容器我們一般首先會想到的是HashMapArrayListHasHSet這幾個容器也是平時開發中用的最多的。這幾個都是非線程安全的,如果我們有特定業務需要使用線程的安全容器列,

  • HashMap可以用ConcurrentHashMap代替。

  • ArrayList 可以使用Collections.synchronizedList()方法(list 每個方法都用synchronized修飾) 或者使用Vector(現在基本也不用了,每個方法都用synchronized修飾) 或者使用CopyOnWriteArrayList 替代。

  • HasHSet 可以使用 Collections.synchronizedSet 或者使用CopyOnWriteArraySet來代替。(CopyOnWriteArraySet爲什麼不叫CopyOnWriteHashSet因爲CopyOnWriteArraySet底層是採用CopyOnWriteArrayList來實現的) 我們可以看到CopyOnWriteArrayList在線程安全的容器裏面多次出現。首先我們來看看什麼是CopyOnWriteCopy-On-Write簡稱COW,是一種用於程序設計中的優化策略。

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

爲什麼要引入COW

防止ConcurrentModificationException異常

在java裏面我們如果採用不正確的循環姿勢去遍歷List時候,如果一邊遍歷一邊修改拋出java.util.ConcurrentModificationException錯誤的。

        List<String> list = new ArrayList<>();
        list.add("張三");
        list.add("java金融");
        list.add("javajr.cn");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String content = iterator.next();
            if("張三".equals(content)) {
                list.remove(content);
            }

        }

上面這個栗子是會發生java.util.ConcurrentModificationException異常的,如果把ArrayList改爲CopyOnWriteArrayList 是不會發生生異常的。

線程安全的容器

我們再看下面一個栗子一個線程往List裏面添加數據,一個線程循環list讀數據。

    List<String> list = new ArrayList<>();
        list.add("張三");
        list.add("java金融");
        list.add("javajr.cn");
        Thread t = new Thread(new Runnable() {
            int count = 0;
            @Override
            public void run() {
                while (true) {
                    list.add(count++ + "");
                }
            }
        });
        t.start();
        Thread.sleep(10000);
        for (String s : list) {
            System.out.println(s);
        }

我們運行上述代碼也會發生ConcurrentModificationException異常,如果把ArrayList換成了CopyOnWriteArrayList就一切正常。

CopyOnWriteArrayList的實現

通過上面兩個栗子我們可以發現CopyOnWriteArrayList是線程安全的,下面我們就來一起看看CopyOnWriteArrayList是如何實現線程安全的。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

從源碼中我們可以知道CopyOnWriteArrayList這和ArrayList底層實現都是通過一個Object的數組來實現的,只不過 CopyOnWriteArrayList的數組是通過volatile來修飾的,還有新增了ReentrantLock

add方法:

    public boolean add(E e) {
        // 先獲取鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 複製一個新的數組
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            // 把新數組的值 賦給原數組
            setArray(newElements);
            return true;
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }

上述源碼我們可以發現比較簡單,有幾個點需要稍微注意下

  • 增加數據的時候是通過ReentrantLock加鎖操作來(在jdk11的時候採用了synchronized來替換ReentrantLock)保證多線程寫的時候只有一個線程進行數組的複製,否則的話內存中會有多份被複制的數據,導致數據錯亂。

  • 數組是通過volatile 修飾的,根據 volatilehappens-before 規則,寫線程對數組引用的修改是可以立即對讀線程是可見的。

  • 通過寫時複製來保證讀寫在兩個不同的數據容器中進行操作。

自己實現一個COW容器

Java併發包裏提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayListCopyOnWriteArraySet,但是並沒有CopyOnWriteHashMap我們可以按照他的思路自己來實現一個CopyOnWriteHashMap

public class CopyOnWriteHashMap<K, V> implements Map<K, V>, Cloneable {

    final transient ReentrantLock lock = new ReentrantLock();

    private volatile Map<K, V> map;


    public CopyOnWriteHashMap() {
        map = new HashMap<>();
    }

    @Override
    public V put(K key, V value) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map<K, V> newMap = new HashMap<K, V>(map);
            V val = newMap.put(key, value);
            map = newMap;
            return val;
        } finally {
            lock.unlock();
        }
    }

    @Override
    public V get(Object key) {
        return map.get(key);
    }
    @Override
    public V remove(Object key) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map<K, V> newMap = new HashMap<K, V>(map);

            if (!newMap.containsKey(key)) {
                return null;
            }
            V v = newMap.get(key);
            newMap.remove(key);
            map = newMap;
            return v;
        }finally {
            lock.unlock();
        }
    }

上述我們實現了一個簡單的CopyOnWriteHashMap,只實現了add、remove、get方法其他剩餘的方法可以自行去實現,涉及到只要數據變化的就要加鎖,讀無需加鎖。

應用場景

CopyOnWrite併發容器適用於讀多寫少的併發場景,比如黑白名單、國家城市等基礎數據緩存、系統配置等。這些基本都是隻要想項目啓動的時候初始化一次,變更頻率非常的低。如果這種讀多寫少的場景採用 Vector,Collections包裝的這些方式是不合理的,因爲儘管多個讀線程從同一個數據容器中讀取數據,但是讀線程對數據容器的數據並不會發生發生修改,所以並不需要讀也加鎖。

CopyOnWrite缺點

CopyOnWriteArrayList雖然是一個線程安全版的ArrayList,但其每次修改數據時都會複製一份數據出來,所以CopyOnWriteArrayList只適用讀多寫少或無鎖讀場景。我們如果在實際業務中使用CopyOnWriteArrayList,一定是因爲這個場景適合而非是爲了炫技。

內存佔用問題

因爲CopyOnWrite的寫時複製機制每次進行寫操作的時候都會有兩個數組對象的內存,如果這個數組對象佔用的內存較大的話,如果頻繁的進行寫入就會造成頻繁的Yong GC和Full GC。

數據一致性問題

CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。讀操作的線程可能不會立即讀取到新修改的數據,因爲修改操作發生在副本上。但最終修改操作會完成並更新容器所以這是最終一致性。

CopyOnWriteArrayList和Collections.synchronizedList()

簡單的測試了下CopyOnWriteArrayList  和  Collections.synchronizedList()的讀和寫發現:

  • 在高併發的寫時CopyOnWriteArray比同步Collections.synchronizedList慢百倍

  • 在高併發的讀性能時CopyOnWriteArray比同步Collections.synchronizedList快幾十倍。

  • 高併發寫時,CopyOnWriteArrayList爲何這麼慢呢?因爲其每次add時,都用Arrays.copyOf創建新數組,頻繁add時內存申請釋放性能消耗大。

  • 高併發讀的時候CopyOnWriteArray無鎖,Collections.synchronizedList有鎖所以讀的效率比較低下。

總結

選擇CopyOnWriteArrayList的時候一定是讀遠大於寫。如果讀寫都差不多的話建議選擇Collections.synchronizedList。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。

  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

  • 感謝您的閱讀,十分歡迎並感謝您的關注。

巨人肩膀摘蘋果

http://ifeve.com/java-copy-on-write/

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣


近期精彩內容推薦:  

 幾句話,離職了

 中國男性的私密數據大賞,女生勿入!

 爲什麼很多人用“ji32k7au4a83”作密碼?

 一個月薪 12000 的北京程序員的真實生活 !


在看點這裏好文分享給更多人↓↓

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章