CopyOnWriteArrayList你都不知道,怎麼拿offer?

前言

只有光頭才能變強

cow

前一陣子寫過一篇COW(Copy On Write)文章,結果閱讀量很低啊...COW奶牛!Copy On Write機制瞭解一下java

可能你們對這個技術比較陌生吧,但這項技術是挺多應用場景的。除了上文所說的Linux、文件系統外,其實在Java也有其身影。git

你們對線程安全容器可能最熟悉的就是ConcurrentHashMap了,由於這個容器常常會在面試的時候考查。github

好比說,一個常見的面試場景:面試

  • 面試官問:「HashMap是線程安全的嗎?若是HashMap線程不安全的話,那有沒有安全的Map容器」
  • 3y:「線程安全的Map有兩個,一個是Hashtable,一個是ConcurrentHashMap」
  • 面試官繼續問:「那Hashtable和ConcurrentHashMap有什麼區別啊?」
  • 3y:「balabalabalabalabalabala"
  • 面試官:」ok,ok,ok,看你Java基礎挺不錯的呀「

那若是有這樣的面試呢?編程

  • 面試官問:「ArrayList是線程安全的嗎?若是ArrayList線程不安全的話,那有沒有安全的相似ArrayList的容器」
  • 3y:「線程安全的ArrayList咱們可使用Vector,或者說咱們可使用Collections下的方法來包裝一下」
  • 面試官繼續問:「嗯,我相信你也知道Vector是一個比較老的容器了,還有沒有其餘的呢?」
  • 3y:「Emmmm,這個...「
  • 面試官提示:「就好比JUC中有ConcurrentHashMap,那JUC中有相似"ArrayList"的線程安全容器類嗎?「
  • 3y:「Emmmm,這個...「
  • 面試官:」ok,ok,ok,今天的面試時間也差很少了,你回去等通知吧。「

今天主要講解的是CopyOnWriteArrayList~數組

本文力求簡單講清每一個知識點,但願你們看完能有所收穫安全

1、Vector和SynchronizedList

1.1回顧線程安全的Vector和SynchronizedList

咱們知道ArrayList是用於替代Vector的,Vector是線程安全的容器。由於它幾乎在每一個方法聲明處都加了synchronized關鍵字來使容器安全。服務器

Vector實現

若是使用Collections.synchronizedList(new ArrayList())來使ArrayList變成是線程安全的話,也是幾乎都是每一個方法都加上synchronized關鍵字的,只不過它不是加在方法的聲明處,而是方法的內部多線程

Collections.synchronizedList()的實現

1.2Vector和SynchronizedList可能會出現的問題

在講解CopyOnWrite容器以前,咱們仍是先來看一下線程安全容器的一些可能沒有注意到的地方~併發

下面咱們直接來看一下這段代碼:

// 獲得Vector最後一個元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }

    // 刪除Vector最後一個元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }

以咱們第一反應來分析一下上面兩個方法:在多線程環境下,是否有問題

  • 咱們能夠知道的是Vector的size()和get()以及remove()都被synchronized修飾的。

答案:從調用者的角度是有問題

咱們能夠寫段代碼測試一下:

import java.util.Vector;

public class UnsafeVectorHelpers {


    public static void main(String[] args) {

        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("關注公衆號");
        vector.add("Java3y");
        vector.add("買Linux可到我下面的連接,享受最低價");
        vector.add("給3y加雞腿");

        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
    }

    // 獲得Vector最後一個元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }

    // 刪除Vector最後一個元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

能夠發現的是,有可能會拋出異常的:

代碼拋出異常

緣由也很簡單,咱們照着流程走一下就行了:

  • 線程A執行getLast()方法,線程B執行deleteLast()方法
  • 線程A執行int lastIndex = list.size() - 1;獲得lastIndex的值是3。同時,線程B執行int lastIndex = list.size() - 1;獲得的lastIndex的值是3
  • 此時線程B先獲得CPU執行權,執行list.remove(lastIndex)將下標爲3的元素刪除了
  • 接着線程A獲得CPU執行權,執行list.get(lastIndex);,發現已經沒有下標爲3的元素,拋出異常了.

交替執行致使異常發生

出現這個問題的緣由也很簡單:

  • getLast()deleteLast()這兩個方法並非原子性的,即便他們內部的每一步操做是原子性的(被Synchronize修飾就能夠實現原子性),可是內部之間仍是能夠交替執行。

    • 這裏的意思就是:size()和get()以及remove()都是原子性的,可是若是併發執行getLast()deleteLast(),方法裏面的size()和get()以及remove()是能夠交替執行的。

要解決上面這種狀況也很簡單,由於咱們都是對Vector進行操做的,只要操做Vector前把它鎖住就沒毛病了

因此咱們能夠改爲這樣子:

// 獲得Vector最後一個元素
    public static Object getLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            return list.get(lastIndex);
        }
    }
    // 刪除Vector最後一個元素
    public static void deleteLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            list.remove(lastIndex);
        }
    }
ps:若是有人去測試一下,發現會拋出異常java.lang.ArrayIndexOutOfBoundsException: -1,這是 沒有檢查角標的異常,不是併發致使的問題。

通過上面的例子咱們能夠看看下面的代碼:

public static void main(String[] args) {

        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("關注公衆號");
        vector.add("Java3y");
        vector.add("買Linux可到我下面的連接,享受最低價");
        vector.add("給3y加雞腿");

        // 遍歷Vector
        for (int i = 0; i < vector.size(); i++) {

            // 好比在這執行vector.clear();
            //new Thread(() -> vector.clear()).start();

            System.out.println(vector.get(i));
        }
    }

一樣地:若是在遍歷Vector的時候,有別的線程修改了Vector的長度,那仍是會有問題

  • 線程A遍歷Vector,執行vector.size()時,發現Vector的長度爲5
  • 此時頗有可能存在線程B對Vector進行clear()操做
  • 隨後線程A執行vector.get(i)時,拋出異常

Vector遍歷拋出異常

在JDK5之後,Java推薦使用for-each(迭代器)來遍歷咱們的集合,好處就是簡潔、數組索引的邊界值只計算一次

若是使用for-each(迭代器)來作上面的操做,會拋出ConcurrentModificationException異常

迭代器遍歷會拋出ConcurrentModificationException

SynchronizedList在使用迭代器遍歷的時候一樣會有問題的,源碼已經提醒咱們要手動加鎖了。

SynchronizedList在遍歷的時候一樣會有問題的

若是想要完美解決上面所講的問題,咱們能夠在遍歷前加鎖

// 遍歷Vector
         synchronized (vector) {
            for (int i = 0; i < vector.size(); i++) {
                vector.get(i);
            }
        }

有經驗的同窗就能夠知道:哇,遍歷一下容器都要我加上鎖,這這這不是要慢死了嗎.的確是挺慢的..

因此咱們的CopyOnWriteArrayList就登場了!

2、CopyOnWriteArrayList(Set)介紹

通常來講,咱們會認爲:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。

不管是Hashtable-->ConcurrentHashMap,仍是說Vector-->CopyOnWriteArrayList。JUC下支持併發的容器與老一代的線程安全類相比,總結起來就是加鎖粒度的問題

  • Hashtable、Vector加鎖的粒度大(直接在方法聲明處使用synchronized)
  • ConcurrentHashMap、CopyOnWriteArrayList加鎖粒度小(用各類的方式來實現線程安全,好比咱們知道的ConcurrentHashMap用了cas鎖、volatile等方式來實現線程安全..)
  • JUC下的線程安全容器在遍歷的時候不會拋出ConcurrentModificationException異常

因此通常來講,咱們都會使用JUC包下給咱們提供的線程安全容器,而不是使用老一代的線程安全容器。

下面咱們來看看CopyOnWriteArrayList是怎麼實現的,爲何使用迭代器遍歷的時候就不用額外加鎖,也不會拋出ConcurrentModificationException異常。

2.1CopyOnWriteArrayList實現原理

咱們仍是先來回顧一下COW:

若是有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取 相同的指針指向相同的資源,直到某個調用者 試圖修改資源的內容時,系統纔會 真正複製一份專用副本(private copy)給該調用者,而其餘調用者所見到的最初的資源仍然保持不變。 優勢是若是調用者 沒有修改該資源,就不會有副本(private copy)被創建,所以多個調用者只是讀取操做時能夠 共享同一份資源

參考自維基百科:https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD

以前寫博客的時候,若是是要看源碼,通常會翻譯一下源碼的註釋並用圖貼在文章上的。Emmm,發現閱讀體驗並非很好,因此我這裏就 直接歸納一下源碼註釋說了什麼吧。另外,若是使用IDEA的話,能夠下一個插件 Translation(免費好用).

Translation插件

Translation插件


歸納一下CopyOnWriteArrayList源碼註釋介紹了什麼:

  • CopyOnWriteArrayList是線程安全容器(相對於ArrayList),底層經過複製數組的方式來實現。
  • CopyOnWriteArrayList在遍歷的使用不會拋出ConcurrentModificationException異常,而且遍歷的時候就不用額外加鎖
  • 元素能夠爲null

2.1.1看一下CopyOnWriteArrayList基本的結構

/** 可重入鎖對象 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** CopyOnWriteArrayList底層由數組實現,volatile修飾 */
    private transient volatile Object[] array;

    /**
     * 獲得數組
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * 設置數組
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 初始化CopyOnWriteArrayList至關於初始化數組
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

看起來挺簡單的,CopyOnWriteArrayList底層就是數組,加鎖就交由ReentrantLock來完成。

2.1.2常見方法的實現

根據上面的分析咱們知道若是遍歷Vector/SynchronizedList是須要本身手動加鎖的。

CopyOnWriteArrayList使用迭代器遍歷時不須要顯示加鎖,看看add()、clear()、remove()get()方法的實現可能就有點眉目了。

首先咱們能夠看看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;
            
            // 將volatile Object[] array 的指向替換成新數組
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

經過代碼咱們能夠知道:在添加的時候就上鎖,並複製一個新數組,增長操做在新數組上完成,將array指向到新數組中,最後解鎖。

再來看看size()方法:

public int size() {

        // 直接獲得array數組的長度
        return getArray().length;
    }

再來看看get()方法:

public E get(int index) {
        return get(getArray(), index);
    }

    final Object[] getArray() {
        return array;
    }

那再來看看set()方法

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        
        // 獲得原數組的舊值
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        // 判斷新值和舊值是否相等
        if (oldValue != element) {
            
            // 複製新數組,新值在新數組中完成
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            
            // 將array引用指向新數組
            setArray(newElements);
        } else {
            // Not quite a no-op; enssures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

對於remove()、clear()set()和add()是相似的,這裏我就再也不貼出代碼了。

總結:

  • 在修改時,複製出一個新數組,修改的操做在新數組中完成,最後將新數組交由array變量指向
  • 寫加鎖,讀不加鎖

2.1.3剖析爲何遍歷時不用調用者顯式加鎖

經常使用的方法實現咱們已經基本瞭解了,但仍是不知道爲啥可以在容器遍歷的時候對其進行修改而不拋出異常。因此,來看一下他的迭代器吧:

// 1. 返回的迭代器是COWIterator
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }


    // 2. 迭代器的成員屬性
    private final Object[] snapshot;
    private int cursor;

    // 3. 迭代器的構造方法
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    // 4. 迭代器的方法...
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    //.... 能夠發現的是,迭代器全部的操做都基於snapshot數組,而snapshot是傳遞進來的array數組

到這裏,咱們應該就能夠想明白了!CopyOnWriteArrayList在使用迭代器遍歷的時候,操做的都是原數組

一張圖來解析COW容器

2.1.4CopyOnWriteArrayList缺點

看了上面的實現源碼,咱們應該也大概能分析出CopyOnWriteArrayList的缺點了。

  • 內存佔用:若是CopyOnWriteArrayList常常要增刪改裏面的數據,常常要執行add()、set()、remove()的話,那是比較耗費內存的。

    • 由於咱們知道每次add()、set()、remove()這些增刪改操做都要複製一個數組出來。
  • 數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性

    • 從上面的例子也能夠看出來,好比線程A在迭代CopyOnWriteArrayList容器的數據。線程B在線程A迭代的間隙中將CopyOnWriteArrayList部分的數據修改了(已經調用setArray()了)。可是線程A迭代出來的是原有的數據。

2.1.5CopyOnWriteSet

CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

3、最後

如今臨近雙十一買阿里雲服務器就特別省錢!以前我買學生機也要9.8塊錢一個月,如今最低價只須要8.3一個月!

若是有要買服務器的同窗可經過個人連接直接享受最低價https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli


閱讀這篇文章可能須要對Java容器和多線程有必定的瞭解。若是對這些知識還不太瞭解的同窗們可看我以前寫過的文章哦~

若是你們有更好的理解方式或者文章有錯誤的地方還請你們不吝在評論區留言,你們互相學習交流~~~

參考資料:

擴展閱讀:

一個 堅持原創的Java技術公衆號:Java3y,歡迎你們關注

3y全部的原創文章:

相關文章
相關標籤/搜索