1. 程式人生 > >Collections.synchronizedList 、CopyOnWriteArrayList、Vector介紹、原始碼淺析與效能對比【文末福利】

Collections.synchronizedList 、CopyOnWriteArrayList、Vector介紹、原始碼淺析與效能對比【文末福利】

ArrayList執行緒安全問題

眾所周知,ArrayList不是執行緒安全的,在併發場景使用ArrayList可能會導致add內容為null,迭代時併發修改list內容拋ConcurrentModificationException異常等問題。java類庫裡面提供了以下三個輪子可以實現執行緒安全的List,它們是

  • Vector
  • Collections.synchronizedList
  • CopyOnWriteArrayList

本文簡要的分析了下它們執行緒安全的實現機制並對它們的讀,寫,迭代效能進行了對比。

Vector

從JDK1.0開始,Vector便存在JDK中,Vector是一個執行緒安全的列表,底層採用陣列實現。其執行緒安全的實現方式非常粗暴:Vector

大部分方法和ArrayList都是相同的,只是加上了synchronized關鍵字,這種方式嚴重影響效率,因此,不再推薦使用Vector了。JAVA官方文件中這樣描述:

If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

如果不需要執行緒安全性,推薦使用ArrayList替代Vector

關鍵原始碼如下:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

public synchronized Iterator<E> iterator() {
    return new Itr();
}    

可以看到Vector通過在方法級別上加入了synchronized關鍵字實現執行緒安全性。

Collections.synchronizedList

因為ArrayList不是執行緒安全的,JDK提供了一個Collections.synchronizedList靜態方法將一個非執行緒安全的List(並不僅限ArrayList)包裝為執行緒安全的List。使用方式如下:

List list = Collections.synchronizedList(new ArrayList());

根據文件,轉換包裝後的list可以實現add,remove,get等操作的執行緒安全性,但是對於迭代操作,Collections.synchronizedList

並沒有提供相關機制,所以迭代時需要對包裝後的list(敲黑板,必須對包裝後的list進行加鎖,鎖其他的不行)進行手動加鎖,使用方式如下:

List list = Collections.synchronizedList(new ArrayList());
//必須對list進行加鎖
synchronized (list) {
  Iterator i = list.iterator();
  while (i.hasNext())
      foo(i.next());
}

這個地方要注意兩個地方:

  1. 迭代操作必須加鎖,可以使用synchronized關鍵字修飾;
  2. synchronized持有的監視器物件必須是synchronized (list),即包裝後的list,使用其他物件如synchronized (new Object())會使add,remove等方法與迭代方法使用的鎖不一致,無法實現完全的執行緒安全性。

通過原始碼可知Collections.synchronizedList生成了特定同步的SynchronizedCollection,生成的集合每個同步操作都是持有mutex這個鎖,所以再進行操作時就是執行緒安全的集合了。關鍵地方已經加了註釋:

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            //ArrayList使用了SynchronizedRandomAccessList類
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}
//SynchronizedRandomAccessList繼承自SynchronizedList
static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess {
}

//SynchronizedList對程式碼塊進行了synchronized修飾來實現執行緒安全性
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
public E get(int index) {
    synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }   
    
    //迭代操作並未加鎖,所以需要手動同步
    public ListIterator<E> listIterator() {
            return list.listIterator(); 
    }
}

CopyOnWriteArrayList

CopyOnWriteArrayListjava.util.concurrent包下面的一個實現執行緒安全的List,顧名思義, Copy~On~Write~ArrayList在進行寫操作(add,remove,set等)時會進行Copy操作,可以推測出在進行寫操作時CopyOnWriteArrayList效能應該不會很高。

先看一下 CopyOnWriteArrayList 的結構:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

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

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    
    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
}

可以看到CopyOnWriteArrayList底層實現為Object[] array陣列。

新增元素:

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

可以看到每次新增元素時都會進行Arrays.copyOf操作,代價非常昂貴。

讀的時候是不需要加鎖的,直接獲取。刪除和增加是需要加鎖的。

有兩點必須講一下。我認為CopyOnWriteArrayList這個併發元件,其實反映的是兩個十分重要的分散式理念:

(1)讀寫分離

我們讀取CopyOnWriteArrayList的時候讀取的是CopyOnWriteArrayList中的Object[] array,但是修改的時候,操作的是一個新的Object[] array,讀和寫操作的不是同一個物件,這就是讀寫分離。這種技術資料庫用的非常多,在高併發下為了緩解資料庫的壓力,即使做了快取也要對資料庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然後讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多。

(2)最終一致

CopyOnWriteArrayList來說,執行緒1讀取集合裡面的資料,未必是最新的資料。因為執行緒2、執行緒3、執行緒4四個執行緒都修改了CopyOnWriteArrayList裡面的資料,但是執行緒1拿到的還是最老的那個Object[] array,新新增進去的資料並沒有,所以執行緒1讀取的內容未必準確。不過這些資料雖然對於執行緒1是不一致的,但是對於之後的執行緒一定是一致的,它們拿到的Object[] array一定是三個執行緒都操作完畢之後的Object array[],這就是最終一致。最終一致對於分散式系統也非常重要,它通過容忍一定時間的資料不一致,提升整個分散式系統的可用性與分割槽容錯性。當然,最終一致並不是任何場景都適用的,像火車站售票這種系統使用者對於資料的實時性要求非常非常高,就必須做成強一致性的。

效能對比

通過前面的分析可知

  • Vector對所有操作進行了synchronized關鍵字修飾,效能應該比較差
  • CopyOnWriteArrayList在寫操作時需要進行copy操作,讀效能較好,寫效能較差
  • Collections.synchronizedList效能較均衡,但是迭代操作並未加鎖,所以需要時需要額外注意

下面寫了個測試程式對三者的讀,寫,遍歷程序了測試來驗證下,測試機器資訊如下:

作業系統:macOS High Sierra 10.13.6
CPU:2.8 GHz Intel Core i7
記憶體:16 GB 2133 MHz LPDDR3

測試程式碼:

**
 * 比較Vector,Collections.synchronizedList,CopyOnWriteArrayList讀操作,寫操作,遍歷操作效能
 *
 * @author nauyus
 * @date 2020年01月29日
 */
public class ListPerformanceTest {

    /**
     * 併發數
     */
    public final static int THREAD_COUNT = 64;
    /**
     * list大小
     */
    public final static int SIZE = 10000;

    /**
     * 測試讀效能
     *
     * @throws Exception
     */
    @Test
    public void testGet() throws Exception {
        List<Integer> list = initList();
        List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(list);
        List<Integer> synchronizedList = Collections.synchronizedList(list);
        Vector vector = new Vector(list);

        int copyOnWriteArrayListTime = 0;
        int synchronizedListTime = 0;
        int vectorTime = 0;
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);

        for (int i = 0; i < THREAD_COUNT; i++) {
            copyOnWriteArrayListTime += executor.submit(new GetTestTask(copyOnWriteArrayList, countDownLatch)).get();
        }
        System.out.println("CopyOnWriteArrayList get method cost time is " + copyOnWriteArrayListTime);

        for (int i = 0; i < THREAD_COUNT; i++) {
            synchronizedListTime += executor.submit(new GetTestTask(synchronizedList, countDownLatch)).get();
        }
        System.out.println("Collections.synchronizedList get method cost time is " + synchronizedListTime);

        for (int i = 0; i < THREAD_COUNT; i++) {
            vectorTime += executor.submit(new GetTestTask(vector, countDownLatch)).get();
        }
        System.out.println("vector get method cost time is " + vectorTime);
    }

    private List<Integer> initList() {
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < SIZE; i++) {
            list.add(new Random().nextInt(1000));
        }
        return list;
    }
    
    class GetTestTask implements Callable<Integer> {
    List<Integer> list;
    CountDownLatch countDownLatch;

        GetTestTask(List<Integer> list, CountDownLatch countDownLatch) {
            this.list = list;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public Integer call() {
            int pos = new Random().nextInt(SIZE);
            long start = System.currentTimeMillis();
            for (int i = 0; i < SIZE; i++) {
                list.get(pos);
            }
            long end = System.currentTimeMillis();
            countDownLatch.countDown();
            return (int) (end - start);
        }
    }

完整版程式碼可以點選閱讀原文或公眾號內回覆文章編號010獲取

測試結果:

可以看到隨著執行緒數的增加,三個類操作時間都有所增加,Vector的遍歷操作和CopyOnWriteArrayList的寫操作(圖片中標紅的部分)效能消耗尤其嚴重。出乎意料的是Vector的讀寫操作和Collections.synchronizedList比起來並沒有什麼差別(印象中Vector效能很差,實際效能差的只是遍歷操作,看來還是紙上得來終覺淺,絕知此事要躬行啊),仔細分析了下程式碼,雖然Vector使用synchronized修飾方法,Collections.synchronizedList使用synchronized修飾語句塊,但實際鎖住內容並沒有什麼區別,效能相似也在情理之中。

總結

  1. CopyOnWriteArrayList的寫操作與Vector的遍歷操作效能消耗尤其嚴重,不推薦使用。

  2. CopyOnWriteArrayList適用於讀操作遠遠多於寫操作的場景。

  3. Vector讀寫效能可以和Collections.synchronizedList比肩,但Collections.synchronizedList不僅可以包裝ArrayList,也可以包裝其他List,擴充套件性和相容性更好。

參考資料:

Java集合:CopyOnWriteArrayList與SynchronizedList

SynchronizedList和Vector的區別

感謝閱讀,如有收穫,求點贊、求關注讓更多人看到這篇文章,本文首發於不止於技術的技術公眾號 Nauyus ,歡迎識別下方二維碼獲取更多內容,主要分享JAVA,微服務,程式語言,架構設計,思維認知類等原創技術乾貨,2019年12月起開啟周更模式,歡迎關注,與Nauyus一起學習。

福利一:後端開發視訊教程

這些年整理的幾十套JAVA後端開發視訊教程,包含微服務,分散式,Spring Boot,Spring Cloud,設計模式,快取,JVM調優,MYSQL,大型分散式電商專案實戰等多種內容,關注Nauyus立即回覆【視訊教程】無套路獲取。

福利二:面試題打包下載

這些年整理的面試題資源彙總,包含求職指南,面試技巧,微軟,華為,阿里,百度等多家企業面試題彙總。 本部分還在持續整理中,可以持續關注。立即關注Nauyus回覆【面試題】無套路獲取。