Java併發-CopyOnWriteArrayList

javaadu發表於2019-07-22

前言

今天我們一起學習下java.util.concurrent併發包裡的CopyOnWriteArrayList工具類。當有多個執行緒可能同時遍歷、修改某個公共陣列時候,如果不希望因使用synchronize關鍵字鎖住整個陣列而影響效能,可以考慮使用CopyOnWriteArrayList。

CopyOnWriteArrayList API

CopyOnWriteArrayList的定義如下:

public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable

它也屬於Java集合框架的一部分,是ArrayList的執行緒安全的變體,跟ArrayList的不同在於:CopyOnWriteArrayList針對陣列的修改操作(add、set等)是基於內部拷貝的一份資料而進行的。換句話說,即使在一個執行緒進行遍歷操作時有其他執行緒可能進行插入或刪除操作,我們也可以“執行緒安全”得遍歷CopyOnWriteArrayList。

例子1:插入(刪除)資料的同時進行遍歷

CopyOnWriteArrayList的實現原理是,在一個執行緒開始遍歷(建立Iterator物件)時,內部會建立一個“快照”陣列,遍歷基於這個快照Iterator進行,在遍歷過程中這個快照陣列不會改變,也就不會丟擲ConcurrentModificationException。如果在遍歷的過程中有其他執行緒嘗試改變陣列的內容,就會拷貝一份新的資料進行變更,而後面再來訪問這個陣列的執行緒,看到的就是變更過的陣列。

  1. 建立一個CopyOnWriteArrayList陣列numbers;

    CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 78});

  2. 建立一個遍歷器iterator;

    Iterator<Integer> iterator = numbers.iterator();

  3. 給numbers中增加(或刪除、修改)一個元素;

    numbers.add(100);

  4. 利用iterator遍歷陣列的元素,發現遍歷的結果是Iterator物件建立之前的;

    List<Integer> result = new LinkedList<>();
    iterator.forEachRemaining(result::add);
    assertThat(result).containsOnly(1, 3, 5, 78);

完整的例子如下:

package org.java.learn.concurrent.copyonwritearraylist;


import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.assertj.core.api.Assertions.*;

/**
 * 作用:
 * User: duqi
 * Date: 2017/11/9
 * Time: 11:20
 */
public class CopyOnWriteArrayListExample {

    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 78});

        Iterator<Integer> iterator = numbers.iterator();
        numbers.add(100);
        List<Integer> result = new LinkedList<>();
        iterator.forEachRemaining(result::add);
        assertThat(result).containsOnly(1, 3, 5, 78);

        Iterator<Integer> iterator2 = numbers.iterator();
        numbers.remove(3);
        List<Integer> result2 = new LinkedList<>();
        iterator2.forEachRemaining(result2::add);
        assertThat(result2).containsOnly(1, 3, 5, 78, 100);
    }
}

例子2:不支援一邊遍歷一邊刪除

由於CopyOnWriteArrayList的實現機制——>修改操作和讀操作拿到的Iterator物件指向的不是一個陣列,因此不支援基於Iterator物件的方法結果的刪除:public void remove();,例子程式碼如下:

package org.java.learn.concurrent.copyonwritearraylist;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 作用: User: duqi Date: 2017/11/9 Time: 13:40
 */
public class CopyOnWriteArrayListExample2 {

    public static void main(String[] args) {
        try {
            testExceptionThrow();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void testExceptionThrow() {
        CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 78});
        Iterator<Integer> integerIterator = numbers.iterator();
        while (integerIterator.hasNext()) {
            integerIterator.remove();
        }
    }
}

結論

CopyOnWriteArrayList適合使用在讀操作遠遠大於寫操作的場景裡,比如快取。發生修改時候做copy,新老版本分離,保證讀的高效能,適用於以讀為主的情況。

參考資料

  1. Guide to CopyOnWriteArrayList
  2. CopyOnWriteArrayList詳解
  3. 官方文件:CopyOnWriteArrayList

本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。
javaadu

相關文章