天天看點

看山聊并發:認識 Java 中的隊列:Vector、ArrayList、CopyOnWriteArrayList、SynchronizedList

看山聊并發:認識 Java 中的隊列:Vector、ArrayList、CopyOnWriteArrayList、SynchronizedList

你好,我是看山。

書接上文,上次聊了聊 在多線程中使用 ArrayList 會發生什麼,這次我們說說平時常用的清單:Vector、ArrayList、CopyOnWriteArrayList、SynchronizedList。

Vector

Vector是在 JDK 1.0 提供的,雖然沒有被标記Deprecated,但是事實上已經沒人使用了。主要原因是性能差,且不符合需求。

從源碼可以看出(這裡不貼源碼了),Vector是基于數組實作,幾乎在所有操作方法上,都用synchronized關鍵字實作方法同步,這種同步方式可以對單一操作進行加鎖,比如多個線程同時執行add會同步阻塞執行,但是多線程執行add和remove時,就不會阻塞了。

但是,大部分需要對隊列加鎖的場景,是想對整個隊列加鎖,而不僅僅是對單一操作加鎖。也就是說,Vector和我們的期望不同,但是又額外增加了同步操作帶來的性能開銷。是以,不是必須使用的場景,都可以使用ArrayList代替,即使是多線程情況下需要同步隊列,也可以使用CopyOnWriteArrayList和SynchronizedList代替。

ArrayList

ArrayList是在 JDK 1.1 提供的,作為Vector的繼任者(ArrayList實作方式與Vector幾乎完全相同),ArrayList把方法上的synchronized全部去掉了,完全沒有實作同步,是非線程安全的。

它的非線程安全,還展現在疊代器的快速失敗上。在使用方法iterator和listIterator建立疊代器之後,如果還對原來的ArrayList隊列進行修改(add 或 remove),疊代器疊代的時候就會報ConcurrentModificationException異常。從源碼可以看出,疊代器在疊代過程中,會檢查隊列中修改次數modCount與建立疊代器時落下的修改次數快照expectedModCount是否相等,相等表示沒有修改過,代碼如下:

private class Itr implements Iterator<E> {
    // 這段代碼是從 ArrayList 中摘取的
    // 隻留下檢查方法,略過其他代碼,有興趣的可以從源碼中檢視
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
      

第三點是在多線程場景中,添加元素可能會丢失資料,或者發生數組越界異常,在多線程中使用 ArrayList 會發生什麼 有較長的描述,這裡就不贅述了。

SynchronizedList

SynchronizedList是Collections的靜态内部類,使用Collections.synchronizedList()靜态方法建立,是一個通過組合List類實作的封裝實作。它的大多數方法通過synchronized (mutex){...}代碼塊同步方式,因為加鎖對象mutex是隊列對象中定義的相同對象,是以對mutex加鎖時,就實作對整個隊列加鎖,也就解決了Vector不能對整個隊列加鎖的問題。是以如果有多個線程同時操作add和remove方法,會阻塞同步執行。

ArrayList中存在的疊代器快速失敗情況,依然存在,正如下面源碼中的注釋:想要使用疊代器,需要使用者手動實作同步。

static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    
    // 代碼摘自 Collections,省略很多代碼

    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }

    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }
}
      

手動同步的時候需要注意,既然我們關注的全局同步,在疊代器設定同步的時候,要保證鎖定對象與add等方法中對象相同。這個在後續補充說明,這裡就不展開了。

CopyOnWriteArrayList

CopyOnWriteArrayList是從 JDK 1.5 開始提供的,先看看add方法的源碼:

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

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

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    // 代碼摘自 CopyOnWriteArrayList,省略很多代碼

    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);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public boolean addAll(Collection<? extends E> c) {
        Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
            ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
        if (cs.length == 0)
            return false;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (len == 0 && cs.getClass() == Object[].class)
                setArray(cs);
            else {
                Object[] newElements = Arrays.copyOf(elements, len + cs.length);
                System.arraycopy(cs, 0, newElements, len, cs.length);
                setArray(newElements);
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

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

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
}
      

可以看到,CopyOnWriteArrayList借助ReentrantLock實作同步,在synchronized優化之前,ReentrantLock性能高于synchronized。CopyOnWriteArrayList也是通過數組實作的,但是在數組前面增加了volatile關鍵字,實作了多線程情況下數組的可見性,更加安全。更重要的一點是,CopyOnWriteArrayList在add添加元素的時候,實作方式是重建數組對象,替換原來的數組引用。與ArrayList的擴容方式相比,減少了空間,但是也增加了指派數組的性能開銷。在get擷取元素的時候,沒有任何鎖,直接資料傳回。

CopyOnWriteArrayList的疊代器時通過COWIterator實作的,調用iterator方法時,将目前隊列中數組的快照指派到疊代器中的數組引用上。如果原來的隊列發生修改,隊列中數組會指向别的引用,而疊代器中的數組不會發生變化,是以在多線程執行過程中,通過疊代器周遊數組,也可以修改隊列中的資料。這種方式保障線程安全的同時,也可能會出現資料不一緻的情況,隻能是使用的使用多注意了。

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;
    }
}
      

對比 CopyOnWriteArrayList 和 SynchronizedList

CopyOnWriteArrayList和SynchronizedList都實作了同步,實作方式上采用的是不同政策,各自的側重點不同。

CopyOnWriteArrayList側重于讀寫分離,發生資料寫操作(add或remove)時,會加鎖,各個線程阻塞執行,執行過程會建立資料副本,替換對象引用;如果同時有讀操作(get或iterator),讀操作讀取的是老資料,或者成為曆史資料快照,或者成為緩存資料。這會造成讀寫同時發生時,資料不一緻的情況,但是資料最終會一緻。這種方式與資料庫讀寫分離模式幾乎相同,很多特性可以類比。

SynchronizedList側重資料強一緻,也就是說當發生資料寫操作(add或remove)時,會加鎖,各個線程阻塞執行,而且也會通過相同的鎖阻塞get操作。

從CopyOnWriteArrayList和SynchronizedList兩種不同僚項方式,可以推斷CopyOnWriteArrayList在寫少讀多的場景中執行效率高,SynchronizedList的讀寫操作效率很均衡,是以在寫多讀多、寫多讀少的場景執行效率都會高于CopyOnWriteArrayList。借用網上的測試結果:

看山聊并發:認識 Java 中的隊列:Vector、ArrayList、CopyOnWriteArrayList、SynchronizedList

文末總結

synchronized關鍵字在 JDK 8 之前性能比較差,可以看到 JDK1.5 之後實作的同步代碼,很多是通過ReentrantLock實作的。

多線程場景中除了需要考慮同步外,還需要考慮資料可見性,可以通過volatile關鍵字實作。

ArrayList完全沒有同步操作,是非線程安全的

CopyOnWriteArrayList和SynchronizedList屬于線程安全隊列

CopyOnWriteArrayList實作讀寫分離,适合場景是寫少讀多的場景

SynchronizedList要求資料強一緻,是隊列全局加鎖方式,讀操作也會加鎖

Vector隻是在疊代器周遊性能很差,如果不考慮全局鎖定隊列,單純讀操作和單獨寫操作性能與SynchronizedList相差不大。

參考

Why is Java Vector (and Stack) class considered obsolete or deprecated?

CopyOnWriteArrayList 與 Collections.synchronizedList 的性能對比

Collections.synchronizedList 、CopyOnWriteArrayList、Vector 介紹、源碼淺析與性能對比

推薦閱讀

如果非要在多線程中使用 ArrayList 會發生什麼?