看了CopyOnWriteArrayList後本身實現了一個CopyOnWriteHashMap

在這裏插入圖片描述

引言

**面試官:** 小夥子你有點眼熟啊,是否是去年來這面試過啊。<br/>java

**二胖:** 啊,沒有啊我這是第一次來這。<br/>面試

**面試官:** 行,那咱們開始今天的面試吧,剛開始咱們先來點簡單的吧,java裏面的容器你知道哪些啊,跟我說一說吧。<br/>數組

**二胖:** 好的,java裏面常見容器有ArrayList(線程非安全)、HashMap(線程非安全)、HashSet(線程非安全),ConcurrentHashMap(線程安全)。<br/>緩存

**面試官:** ArrayList 既然線程非安全那有沒有線程安全的ArrayList列?<br/>安全

**二胖:** 這個。。。 好像問到知識盲點了。<br/>多線程

**面試官:** 那咱們今天的面試就先到這了,我待會還有一個會,後續若有通知人事會聯繫你的。<br/>併發

**以上故事純屬虛構若有雷同請以本文爲主。**app

什麼是COW

在java裏面說到集合容器咱們通常首先會想到的是HashMapArrayListHasHSet這幾個容器也是平時開發中用的最多的。dom

這幾個都是非線程安全的,若是咱們有特定業務須要使用線程的安全容器列,ide

  • 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錯誤的。

若是對ArrayList循環遍歷不是很熟悉的能夠建議看下這篇文章《ArrayList的刪除姿式你都掌握了嗎》

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來修飾的,爲何須要volatile修飾建議能夠看看《Java的synchronized 能防止指令重排序嗎?》

還有新增了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。

結束

  • 因爲本身才疏學淺,不免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。

  • 若是你以爲文章還不錯,你的轉發、分享、讚揚、點贊、留言就是對我最大的鼓勵。

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


8888.png

巨人肩膀摘蘋果

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

相關文章
相關標籤/搜索