《Java併發編程之美》讀書筆記二【CopyOnWriteArrayList】

一、併發List簡答介紹

  • 因爲併發包中只有CopyOnWriteArrayList ,CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行的修改操作都是在底層的一個複製的數組(快照)上進行的,也就是使用了寫時複製策略。

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;

CopyOnWriteArrayList 對象裏面有一個array數組對象用來存放具體元素,ReentrantLock 獨佔鎖對象用來保證同時只有一個線程對array進行修改。

  • 寫時複製的線程安全的list需要注意以下幾點:

何時初始化list,初始化的list 元素個數爲多少,list是有限大小嗎?
如何保證線程安全,比如多個線程進行讀寫時如何保證是線程安全的?
如何保證使用迭代器遍歷List時的數據一致性?

二、源碼解析

1、初始化:
  • 無參構造函數
public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

代碼內部創建了一個大小爲 0 的 Object數組作爲Array的初始值。

  • 有參構造函數:
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

創建一個 List 其內部元素是入參toCopyIn的副本

public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

入參是一個集合,將集合裏的元素複製到本List

2、添加元素:

在這裏插入圖片描述

  • 上圖中的原理類似主要看一下add(E e)
public boolean add(E e) {
		//獲取獨佔鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//獲取array
            Object[] elements = getArray();
            int len = elements.length;
            //複製array 到新數組,
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加元素到新數組
            newElements[len] = e;
            //使用新數組替換添加前的數組
            setArray(newElements);
            return true;
        } finally {
        	//釋放獨佔鎖
            lock.unlock();
        }
    }
//獲取數組array
final Object[] getArray() {
        return array;
    }
//替換數組
final void setArray(Object[] a) {
        array = a;
    }

調用add 方法的線程會首先執行代碼中獲取獨佔鎖的部分,進行鎖的獲取,如果duoge線程都調用add 方法則只有一個線程會獲取到該鎖,其他線程會被阻塞掛起直到鎖被釋放。
所以一個線程獲取到鎖後,就保證了在該線程添加元素的過程中其他線程不會對array進行修改。
獲取鎖後,獲取數組,獲取到數組後,將該數組複製到一個新的數組【將原來的數組長度➕1】,將新增的元素添加到新數組。
使用新數組替換原數組,並在返回前釋放鎖,由於加了鎖,整個add 過程是個原子性的操作,【注意: 添加元素的時候,是在 副本上進行的操作,而不是在原數組上進行操作】。

3、獲取指定位置元素:
	/**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
  • getArray 和上面的一樣的,看一下 get方法:
	@SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

使用get(int index) 獲取下標爲index 的元素,如果元素不存在則拋出IndexOutOfBoundsException異常。
在這裏插入圖片描述

當線程X 調用get 方法獲取指定位置的元素時,分兩步: 1、首先獲取array數組【一】,2、通過下標訪問指定位置的元素【2】。但是我們要注意到整個過程並沒有加鎖同步。

因爲這兩部都沒有加鎖,可能線程X執行完步驟一 之後,在步驟2 之前,另外一個線程y 進行了remove 操作,假設刪除元素1 ,remove會首先獲取獨佔鎖,然後進行寫時複製操作,也就是複製一份當前array數組,然後在複製的數組裏刪除 1 這個元素,之後讓array 指向複製的數組,而此時,array 之前指向的數組的引用計數器爲1 而不是0 ,因爲線程X 還在使用它,這時線程X 開始執行步驟2,步驟2 操作的數組是 線程Y 刪除元素之前的數組。

  • 所以雖然Y 已經刪除了index 處的元素,但是線程X的步驟還是會返回index 處的鎖,這是寫時複製策略產生的弱一致性問題。
4、修改指定元素:
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;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

首先獲取獨佔鎖,從而阻止其他線程對array數組進行修改,然後獲取當前數組,並調用get方法獲取指定位置的元素,如果指定位置的元素值與新值不一致則創建新數組並複製元素,然後在新數組上修改指定位置的元素值並設置新數組到array,如果指定位置的元素值與新值一樣,則爲了保證volatile的語義,還是需要重新設置array,雖然array 的內容並沒有改變。

5、刪除元素:

在這裏插入圖片描述

  • 刪除list中指定元素可以使用上圖中的方法,原理大致一樣,主要講一下remove(int index)
public E remove(int index) {
		//獲取獨佔鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//獲取數組
            Object[] elements = getArray();
            int len = elements.length;
            //獲取指定元素
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            //如果刪除的是最後一個元素
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
            	//分兩次複製刪除後剩餘的元素到新數組
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                //使用新數組代替老數組
                setArray(newElements);
            }
            return oldValue;
        } finally {
        	//釋放鎖
            lock.unlock();
        }
    }

類似於新增元素的方法,首先獲取獨佔鎖,保證刪除數據期間其他線程無法對array進行修改,然後獲取數組中要被刪除的元素,並把剩餘的元素複製到新數組,之後使用新數組替換原來的老數組,最後在返回前釋放鎖。

6、弱一致性的迭代器:

所謂的弱一致性是指返回迭代器後,其他線程對List的刪除改對迭代器是不可見的。前面在獲取指定位置的元素試提到過。
使用iterator獲取迭代器時會返回COWIterator 對象,COWIterator 存儲着一個array的快照,snapshot,說snapshot是list的快照的原因是:遍歷元素的過程中,其他線程沒有對list進行增刪改,那麼snapshot本身就是list的array,因爲它們是引用關係,但是如果遍歷期間其他線程對該list進行了增刪改,那麼snapshot就是快照了,因爲增刪改後,list裏的數組被新數組替代了,這時候老數組被snapshot引用。

這就說明獲取迭代器後,使用該迭代器元素時,其他線程對該list進行的增刪改不可見。因爲操作的其實是兩個數組,這就是弱一致性。

  • 這個地方和之前的獲取指定位置的元素很像。

三、總結

CopyOnWriteArrayList 使用寫時複製的策略來保證list的一致性,而 獲取 - 修改 - 寫入 這三個步驟並不是原子的,所以在增刪改的過程中都使用了獨佔鎖,來保證某個某個時間內只有一個線程對list數組進行修改。CopyOnWriteArrayList 還提供了 弱一致性的迭代器從而保證在獲取迭代器後,其他線程對list的修改是不可見的。迭代器遍歷的數組是一個快照。

CopyOnWriteArraySet 底層使用 CopyOnWriteArrayList 實現的。

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