爲何線程安全的List推薦使用CopyOnWriteArrayList,而不是Vector

注:本系列文章中用到的jdk版本均爲 java8

相比不少同窗在剛接觸Java集合的時候,線程安全的List用的必定是Vector。可是如今用到的線程安全的List通常都會用CopyOnWriteArrayList,不多有人再去用Vector了,至於爲何,文章中會具體說到。接下來,咱們先來簡單分析如下Vector的源碼。java

1、Vector集合源碼簡析

因爲本文的重點不是Vector集合,所以只是簡單的分析一下Vector的初始化方法和添加元素的方法。面試

Vector的底層實現和ArrayList同樣,都是由數組實現的。數組

Vector的主要變量以下:安全

/**
 * 存放元素的數組
 */
protected Object[] elementData;
/**
 * 元素個數
 */
protected int elementCount;
/**
 * 擴容自增容量大小
 */
protected int capacityIncrement;

1.1 Vector初始化

Vector的初始化提供了三個方法,除了能夠指定初始容量的大小,還能夠指定擴容容量的大小。構造器分別以下:微信

無參構造器多線程

public Vector() {
    this(10);
}

指定初始化容量的構造器dom

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

指定初始化容量和擴容容量大小的構造器性能

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

從上面的構造器中能夠看出,若是調用無參構造器,則會建立一個初始化容量爲10,擴容容量爲0Vector集合。this

1.2 如何擴容

Vector的擴容機制和ArrayList的很像,若是不清楚ArrayList的擴容機制,能夠看看這篇文章。這裏咱們直接看Vector的擴容方法growspa

private void grow(int minCapacity) {
    // overflow-conscious code
    // 初始化數組的長度,默認爲10
    int oldCapacity = elementData.length;
    // 是否指定擴容容量,不指定擴容爲原來的2倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

經過上面的方法,咱們能夠看出,若是指定了擴容容量的大小則擴容的新數組大小爲原來的數組加上擴容容量的大小,若是不指定擴容容量的大小則擴容的新數組大小爲原來數組大小的2倍。這樣擴容爲原來的2倍是很消耗空間的,這也是Vector被棄用的緣由之一。

除此以外,看過源碼的同窗可能發現了,Vector集合的全部操做元素的方法都加了synchronized關鍵字,這就致使了操做Vector的效率會很是低,在開發中,每每讀操做的使用頻率會遠高於其餘操做,而CopyOnWriteArrayList 就是這樣一種讀操做效率遠高於寫操做效率的List,一塊兒來看看。

2、CopyOnWriteArrayList源碼簡析

CopyOnWriteArrayList 類圖:

2.1 CopyOnWrite思想

CopyOnWrite簡稱COW,根據名字來看就是寫入時複製。意思就是你們共同去訪問一個資源,若是有人想要去修改這個資源的時候,就須要複製一個副本,去修改這個副本,而對於其餘人來講訪問得資源仍是原來的,不會發生變化。

2.2 初始化CopyOnWriteArrayList

CopyOnWriteArrayList 底層是也是有數組實現的。 本文咱們只解讀添加元素和讀取元素的區別,刪除修改元素原理和添加元素差很少,操做時都須要進行加鎖,而讀操做不會加鎖。

CopyOnWriteArrayList 主要有如下兩個變量:

// 獨佔鎖
final transient ReentrantLock lock = new ReentrantLock();

// 存放元素的數組
private transient volatile Object[] array;

咱們仔細來分析一下上面兩個屬性,這兩個思想是 CopyOnWriteArrayList 的核心 。

  • lock:ReentrantLock,獨佔鎖,多線程運行的狀況下,只有一個線程會得到這個鎖,只有釋放鎖後其餘線程才能得到。
  • array:存放數據的數組,關鍵是被volatile修飾了,被volatile修飾,就保證了可見性,也就是一個線程修改後,其餘線程當即可見。

最經常使用的初始化方式以下:

/**
  * Creates an empty list.
  */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

/**
  * Sets the array.
  */
final void setArray(Object[] a) {
    array = a;
}

初始化只是建立了一個空的數組,並將array指向它。

2.3 添加元素

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 獲取原來的數組
        Object[] elements = getArray();
        // 原來數組的長度
        int len = elements.length;
        // 建立一個長度+1的新數組,並將原來數組的元素複製給新數組
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 元素放在新數組末尾
        newElements[len] = e;
        // array指向新數組
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

添加數組的步驟以下:

  1. 得到獨佔鎖,將添加功能加鎖
  2. 獲取原來的數組,並獲得其長度
  3. 建立一個長度爲原來數組長度+1的數組,並拷貝原來的元素給新數組
  4. 追加元素到新數組末尾
  5. 指向新數組
  6. 釋放鎖

這個過程是線程安全的,COW的核心思想就是每次修改的時候拷貝一個新的資源去修改,add()方法再拷貝新資源的時候將數組容量+1,這樣雖然每次添加元素都會浪費必定的空間,可是數組的長度正好是元素的長度,也在必定程度上節省了擴容的開銷。

2.4 獲取元素

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

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

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

讀操做是自然安全的操做,並且數組自己會進行檢查越界問題,所以獲取元素的方法很簡單,只是根據索引獲取該元素。

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

因爲CopyOnWriteArrayList的底層數組長度,自己就是元素大小,所以size()方法只要返回數組長度就能夠了。

3、總結

VectorCopyOnWriteArrayList都是線程安全的List,底層都是數組實現的,Vector的每一個方法都進行了加鎖,而CopyOnWriteArrayList的讀操做是不加鎖的,所以CopyOnWriteArrayList的讀性能遠高於VectorVector每次擴容的大小都是原來數組大小的2倍,而CopyOnWriteArrayList不須要擴容,經過COW思想就能使數組容量知足要求。兩個集合都是先了RandomAccess接口,支持隨機讀取,所以更加推薦使用for循環進行遍歷。在開發中,讀操做會遠遠多於其餘操做,所以使用CopyOnWriteArrayList集合效率更高。

點關注、不迷路

若是以爲文章不錯,歡迎關注、點贊、收藏,大家的支持是我創做的動力,感謝你們。

若是文章寫的有問題,請不要吝惜文筆,歡迎留言指出,我會及時覈查修改。

若是你還想看到更多別的東西,能夠微信搜索「Java旅途」進行關注。「Java旅途」目前已經整理各類中間件的使用教程及各種Java相關的面試題。掃描下方二維碼進行關注就能夠獲得這些資料。

相關文章
相關標籤/搜索