天天看點

了解另類的并發安全實作CopyOnWriteArrayList

了解另類的并發安全實作CopyOnWriteArrayList

在Java的并發包java.util.concurrent裡面有一個比較有意思現象,針對Map和LinkList都有對應的高效的+線程安全的并發實作類:

ConcurrentHashMap
ConcurrentLinkedQueue           

複制

唯獨沒有針對ArrayList的高效的并發實作,這個我們後面在細說,先來看下目前在Java裡面線程安全的List有三種:

Vector 
Collections.synchronizedList(List list)
CopyOnWriteArrayList           

複制

Vector這個類是一個非常古老的類了,在JDK1.0的時候便已經存在,其實作安全的手段非常簡單所有的方法都加上synchronized關鍵字,這樣保證這個執行個體的方法同一時刻隻能有一個線程通路,是以在高并發場景下性能非常低。

Collections.synchronizedList(List list)這個方法,其實在内部有一個SynchronizedList包裝類對應,其實作安全的手段稍微有一點優化,就是把Vector加在方法上的synchronized關鍵字,移到了方法裡面變成了同步塊而不是同步方法進而把鎖的範圍縮小了,而且在構造函數的時候可以傳入不同的同步螢幕,實作基于不同的螢幕并發,但我覺得沒有多大意義,隻能保證一個螢幕内是線程安全的,不同螢幕還是不安全的,除非另一個是隻讀操作,但如果是這樣,完全可以用并發包的讀寫鎖來替代。

CopyOnWriteArrayList這個類比較特殊,對于寫作來說是基于重入鎖互斥的,對于讀操作來說是無鎖的。還有一個特殊的地方,這個類的iterator是fail-safe的,也就是說是線程安全List裡面的唯一一個不會出現ConcurrentModificationException異常的類。能做到這一點其實是有代價的,跟它的實作機制有很大關系,我們從名字上就能看出來,CopyOnWriteArrayList這個類内部維護的核心内容:

//重入鎖保寫操作互斥
    final transient ReentrantLock lock = new ReentrantLock();
    //volatile保證讀可見性
    private transient volatile Object[] array;           

複制

從上面的代碼能夠看出,為了實作無鎖讀,對象數組上面是加了volatile修飾的,當然如果你去掉這個關鍵字,那麼對于讀操作來說也是必須加鎖的。volatile相比讀加鎖實作則更輕量級。

接着我們看這個類是如何添加資料的:

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加鎖
        try {
            Object[] elements = getArray();//讀取原數組
            int len = elements.length;
            //建構一個長度為len+1的新數組,然後拷貝舊資料的資料到新數組
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //把新加的資料指派到最後一位
            newElements[len] = e;
            // 替換舊的數組
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }           

複制

簡單的說,這個類每次新加的資料都會新copy生成一個數組來容納,并不是直接修改原來的資料結構,這種方式提供了安全的快照讀和周遊的方法,帶來的不足就是對于頻繁寫的應用并不适合,Doug Lea大神在開發這個類的時候也介紹了這個類的主要應用場景是避免對集合的iterator方法加鎖周遊。

這裡要提一下對于對于Java裡面的集合類無論是線程安全和不安全的,隻要涉及到在周遊的時候修改資料,就會抛出異常,原因是集合類的modCount字段與Iteritor記錄的expectedModCount字段值不相等,也就是不同步導緻的,這是集合類的fail-fast機制,在單線程情況下我們可以通過Iteritor的remove方法來避免抛出ConcurrentModificationException異常,但在多線程情況下仍然是有問題的,如果想要解決,有兩種方式:

(1)在周遊IterItor時候,采用加鎖政策,避免多個線程同時修改。

(2)采用弱一緻性的副本,原理是不改變原來資料,比如CopyOnWriteArrayList這種的。這種解決方法非常類似資料庫技術的MVCC多版本的模式,對于Iteritor生成的時候,讀取的是目前數組的快照,是以在周遊的時候永遠不會存在ConcurrentModificationException異常,注意CopyOnWriteArrayList是不支援Iteritor.remove操作的,因為對快照的删除是沒有任何意義的,是以想要删除必須調用CopyOnWriteArrayList.remove方法,前面說過該類的寫操作相關的方法都是線程互斥的,是以不存在安全問題。

整體來說CopyOnWriteArrayList是另類的線程安全的實作,但并一定是高效的,适合用在讀取和周遊多的場景下,并不适合寫并發高的場景,因為數組的拷貝也是非常耗時的,尤其是資料量大的情況下。

到這裡我們能夠看到關于List的線程安全實作基本都是采用加鎖實作,隻不過CopyOnWriteArrayList是比較特殊的另類的安全并發實作,包括同樣的CopyOnWriteArraySet(底層用的CopyOnWriteArrayList),這裡強調了線程安全,但并沒有提到高效,因為HashMap和LinkQueue都有對應的線程安全+高效的并發容器,隻有List沒有,主要原因如下:

在Java并發程式設計網有一篇關于這個的解釋,我在這裡總結一下:

在java.util.concurrent包中沒有加入并發的ArrayList實作的主要原因是:很難去開發一個通用并且沒有并發瓶頸的線程安全的List。

1:ConcurrentHashMap這樣的類的真正價值在于,在保證了線程安全的同時,采用了分段鎖+弱一緻性的Map疊代器提供了高效的并發效率。如果僅僅是線程安全而不高效,對于高并發來說意義不大。

2:ConcurrentLinkedQueue基于連結清單的高并發實作采用了CAS +自旋的方式,提供了無阻塞的高并發實作,主要原因是他們的接口相比List的接口有更多的限制,這些限制使得實作并發成為可能。

而ArrayList這樣的資料結,不知道如何去規避并發的瓶頸,拿contains() 這樣一個操作來說,當你進行搜尋的時候如何避免鎖住整個list?

CopyOnWriteArrayList的實作僅僅是規避了讀并發的瓶頸,對于修改操作扔然是需要鎖住整個List的,是以從某種程度上來說,實作一個通用高效的并發List是比較困難的,這也是java并發包裡為什麼沒有該實作的原因。

本文主要介紹了Java并發包裡面另類的安全實作方式CopyOnWriteArrayList的實作原理,其主要特點是利用了快照的概念進而使讀和疊代器周遊操作無須同步加鎖,由于其不可變的特性,是以在并發應用中更加容易處理,但這種方式也是有代價的畢竟數組的拷貝在資料大的時候也是一筆不少的開銷,這一點需要注意。