Java多線程進階(二七)—— J.U.C之collections框架:CopyOnWriteArrayList

圖片描述

本文首發於一世流雲專欄: https://segmentfault.com/blog...

1、CopyOnWriteArrayList簡介

ArrayList是一種「列表」數據機構,其底層是經過數組來實現元素的隨機訪問。JDK1.5以前,若是想要在併發環境下使用「列表」,通常有如下3種方式:java

  1. 使用Vector
  2. 使用Collections.synchronizedList返回一個同步代理類;
  3. 本身實現ArrayList的子類,並進行同步/加鎖。

前兩種方式都至關於加了一把「全局鎖」,訪問任何方法都須要首先獲取鎖。第3種方式,須要本身實現,複雜度較高。segmentfault


JDK1.5時,隨着J.U.C引入了一個新的集合工具類——CopyOnWriteArrayList數組

clipboard.png

大多數業務場景都是一種「讀多寫少」的情形,CopyOnWriteArrayList就是爲適應這種場景而誕生的。併發

CopyOnWriteArrayList,運用了一種「寫時複製」的思想。通俗的理解就是當咱們須要修改(增/刪/改)列表中的元素時,不直接進行修改,而是先將列表Copy,而後在新的副本上進行修改,修改完成以後,再將引用從原列表指向新列表。dom

這樣作的好處是讀/寫是不會衝突的,能夠併發進行,讀操做仍是在原列表,寫操做在新列表。僅僅當有多個線程同時進行寫操做時,纔會進行同步。工具

2、CopyOnWriteArrayList原理

內部結構

CopyOnWriteArrayList的字段很簡單:大數據

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    /**
     * 排它鎖, 用於同步修改操做
     */
    final transient ReentrantLock lock = new ReentrantLock();

    /**
     * 內部數組
     */
    private transient volatile Object[] array;
}

其中,lock用於對修改操做進行同步,array就是內部實際保存數據的數組。this


構造器定義spa

CopyOnWriteArrayList提供了三種不一樣的構造器,這三種構造器最終都是建立一個數組,並經過setArray方法賦給array字段:線程

/**
 * 空構造器.
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
 
僅僅是設置一個了大小爲0的數組,並賦給字段array:
final void setArray(Object[] a) {
    array = a;
}
/**
 * 根據已有集合建立
 */
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);
}
/**
 * 根據已有數組建立.
 *
 * @param toCopyIn the array (a copy of this array is used as the
 *                 internal array)
 * @throws NullPointerException if the specified array is null
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

核心方法

查詢——get方法

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

private E get(Object[] a, int index) {
    return (E) a[index];
}

能夠看到,get方法並無加鎖,直接返回了內部數組對應索引位置的值:array[index]


添加——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);              // 內部array引用指向新數組
        return true;
    } finally {
        lock.unlock();
    }
}

add方法首先會進行加鎖,保證只有一個線程能進行修改;而後會建立一個新數組(大小爲n+1),並將原數組的值複製到新數組,新元素插入到新數組的最後;最後,將字段array指向新數組。

clipboard.png

上圖中,ThreadB對Array的修改因爲是在新數組上進行的,因此並不會對ThreadA的讀操做產生影響。


刪除——remove方法

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)                  // index位置恰好是最後一個元素
            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指向新數組。


其它統計方法

public int size() {
    return getArray().length;
}

public boolean isEmpty() {
    return size() == 0;
}

迭代

CopyOnWriteArrayList對元素進行迭代時,僅僅返回一個當前內部數組的快照,也就是說,若是此時有其它線程正在修改元素,並不會在迭代中反映出來,由於修改都是在新數組中進行的。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
 
static final class COWIterator<E> implements ListIterator<E> {
    /**
     * Snapshot of the array
     */
    private final Object[] snapshot;
    /**
     * Index of element to be returned by subsequent call to next.
     */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public E next() {
        if (!hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    
    // ...
}

能夠看到,上述iterator方法返回一個迭代器對象——COWIterator,COWIterator的迭代是在舊數組上進行的,當建立迭代器的那一刻就肯定了,因此迭代過程當中不會拋出併發修改異常——ConcurrentModificationException

另外,迭代器對象也不支持修改方法,所有會拋出UnsupportedOperationException異常。

3、總結

CopyOnWriteArrayList的思想和實現總體上仍是比較簡單,它適用於處理「讀多寫少」的併發場景。經過上述對CopyOnWriteArrayList的分析,讀者也應該能夠發現該類存在的一些問題:

1. 內存的使用
因爲CopyOnWriteArrayList使用了「寫時複製」,因此在進行寫操做的時候,內存裏會同時存在兩個array數組,若是數組內存佔用的太大,那麼可能會形成頻繁GC,因此CopyOnWriteArrayList並不適合大數據量的場景。

2. 數據一致性CopyOnWriteArrayList只能保證數據的最終一致性,不能保證數據的實時一致性——讀操做讀到的數據只是一份快照。因此若是但願寫入的數據能夠馬上被讀到,那CopyOnWriteArrayList並不適合。

相關文章
相關標籤/搜索