【JUC】COW容器淺析

1、什麼是COW

維基百科定義:
  寫入時複製(英語:Copy-on-write,簡稱COW)是一種計算機程序設計領域的優化策略。其核心思想是,如果有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明的(transparently)。
  此作法主要的優點是如果調用者沒有修改該資源,就不會有副本(private copy)被創建,因此多個調用者只是讀取操作時可以共享同一份資源。
大白話:
  通俗的講就是,cow基於“寫時複製”的思想,當併發請求讀取相同的數據資源時,讀取的是同一份數據,當某個請求嘗試去修改資源時,就會通過複製出一個副本進而去修改副本的數據,其他請求讀取的數據還是最初不變的,最後將指向源數據的引用指向此線程修改後的副本數據。

2、Java中的Cow容器

  Java中的Cow容器有兩個,在Jdk1.5版本中開始出現的,分別是CopyOnWriteArrayList及CopyOnWriteArraySet。
  CopyOnWriteArrayList與CopyOnWriteArraySet基本一致,主要區別是在add方法,CopyOnWriteArraySet,有set的特性,即存儲元素的是不重複的,因此CopyOnWriteArraySet的add方法中使用的是addIfAbsent(E e),即只有當元素不存在的時候,纔會將元素添加到集合的尾部。

3、CopyOnWriteArrayList源碼分析

  從源碼中,可以看出CopyOnWriteArrayList內部持有一個ReentrantLock鎖,最重要屬性array是一個Object類型的數組並且有volatile關鍵字修飾,而且這個array只能通過getArray()和setArray()來進行訪問。

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

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    /**
     * 獲取源數組
     */
    final Object[] getArray() {
        return array;
    }

    /**
     *將源數組引用指向新數組
     */
    final void setArray(Object[] a) {
        array = a;
    }

add()方法:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 1、加鎖
        lock.lock();
        try {
        	// 2、獲取源數組引用
            Object[] elements = getArray();
            int len = elements.length;
        	// 3、拷貝出一個新數組,新數組長度=源數組長度 + 1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 4、將元素添加到新數組的尾部
            newElements[len] = e;
            // 5、將源數組引用指向新數組
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

add()方法主要工作流程如下:

  • 先進行加鎖操作
  • 通過getArray()方法獲取源數組
  • 拷貝出一個新數組,並且新數組的長度加1
  • 將要添加的元素追加到新數組的尾部
  • 通過setArray()方法將源數組引用指向新數組
  • 最後釋放鎖

remove()方法

public boolean remove(Object o) {
	// 1、獲取數組快照
    Object[] snapshot = getArray();
    // 2、獲取要移除元素的索引下標
    int index = indexOf(o, snapshot, 0, snapshot.length);
    // 3、若未找到,則直接返回false,否則調用remove方法進行移除
    return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    // 若要移除的元素爲null,則直接遍歷數組,找到第一個值爲null的數組下標返回                       
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
    // 遍歷數組,通過equals方法找到要移除的元素的下標返回
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    // 未找到要刪除的元素,默認返回-1
    return -1;
}
private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    // 1、加鎖
    lock.lock();
    try {
    	// 2、獲取源數組
        Object[] current = getArray();
        int len = current.length;
        // 3、若快照與當前數組不等,則說明併發情況下源數組已經被改變
        if (snapshot != current) findIndex: {
        	// 取較小數組長度
            int prefix = Math.min(index, len);
            // 遍歷判斷是否能能找到要刪除的元素的下標,若找到則跳出if語句
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            // 若之前定位的數組下標大於當前源數組長度,則直接返回false
            if (index >= len)
                return false;
            // 若源數組中索引下標位置的元素與要刪除的元素相等,則跳出if語句
            if (current[index] == o)
                break findIndex;
            // 遍歷獲取要刪除的元素下標
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        // 創建新數組
        Object[] newElements = new Object[len - 1];
        // 將當前數組中索引下標位置之前的元素先拷貝到新數組中
        System.arraycopy(current, 0, newElements, 0, index);
        // 將當前數組中索引下標位置止嘔的元素再拷貝到新數組中
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        // 通過setArray()方法將源數組引用指向新數組
        setArray(newElements);
        return true;
    } finally {
    	// 解鎖
        lock.unlock();
    }
}

remove()方法的工作流程其實也不復雜,順着源碼往下看就能理順,主要流程如下:

  • 先在無鎖的情況下,在當前數組中尋找要移除的元素下標,若未找到,則直接返回,否則調用重寫的remove()方法
  • 調用重寫的remove()方法,會先進行加鎖操作
  • 然後看當前數組在加鎖前是否已經發生變化(和未加鎖時獲取的源數組進行比較),因爲併發情況下,源數組可能已經被其他修改操作修改而發生變化。
  • 若當前數組發生變化,則嘗試在獲取要刪除的元素的下標
  • 找到要移除的元素下標,則生成新數組,並進行數組元素拷貝;否則直接返回
  • 最後將源數組引用指向新數組
  • 最後再釋放鎖

4、COW容器優缺點及適用場景

  從源碼中我們就能體會到,cow容器提供了在修改操作時,採用複製新數組的方式,並在修改操作(添加或刪除)中加鎖,讀取操作在併發情況下並不能保證讀取的元素是最新的,但是修改操作會保證數據的最終一致性。
優點:

  • 在多線程併發場景中,以犧牲空間來換取數據被併發修改的最終一致性,尤其適合讀多寫少的場景
  • 併發修改操作(添加或刪除)不會出現併發修改異常(ConcurrentModificationException)

缺點:

  • 不適合寫操作比較多的場景,寫操作由於加鎖,會影響性能
  • 修改操作,會額外佔用一倍的內存空間

適用場景:

  • 多線程併發情況下,且讀多寫少的場景,容忍犧牲一部分空間來換取多線程環境下的數據的最終一致性
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章