溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JUC的CopyOnWriteArrayList怎么理解

發(fā)布時間:2021-12-21 10:18:16 來源:億速云 閱讀:159 作者:iii 欄目:大數(shù)據(jù)

這篇文章主要介紹“JUC的CopyOnWriteArrayList怎么理解”,在日常操作中,相信很多人在JUC的CopyOnWriteArrayList怎么理解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JUC的CopyOnWriteArrayList怎么理解”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

CopyOnWriteArrayList 是線程安全的 List 實現(xiàn),底層依賴于數(shù)組作為存儲結(jié)構(gòu),并基于 寫時復制(CoW: Copy-on-Write)機制 保證線程安全性。CopyOnWriteArrayList 在執(zhí)行修改操作時會將底層數(shù)組復制一份,并在副本上實施修改,最后再更新回底層數(shù)組。雖然這樣的實現(xiàn)比較消耗內(nèi)存,但卻帶來了較高的執(zhí)行效率,屬于拿空間換時間。

除了 CopyOnWriteArrayList,在 JUC 包中還有另外一個基于 CoW 機制實現(xiàn)的線程安全組件,即 CopyOnWriteArraySet,不過 CopyOnWriteArraySet 本質(zhì)上還是基于 CopyOnWriteArrayList 實現(xiàn)的,所以理解了 CopyOnWriteArrayList 的實現(xiàn)原理,也就同時理解了 CopyOnWriteArraySet 的實現(xiàn)原理。

CopyOnWriteArrayList 實現(xiàn)內(nèi)幕

CopyOnWriteArrayList 實現(xiàn)自 List 接口,所以我們可以像使用 ArrayList 一樣使用 CopyOnWriteArrayList。CopyOnWriteArrayList 類的字段定義如下:

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

    /** 支撐同步操作的重入鎖 */
    final transient ReentrantLock lock = new ReentrantLock();
    /** 底層數(shù)組 */
    private transient volatile Object[] array;

    // ... 省略方法定義

}

其中 CopyOnWriteArrayList#array 數(shù)組是 CopyOnWriteArrayList 的底層存儲結(jié)構(gòu),CopyOnWriteArrayList 依賴于該數(shù)組對數(shù)據(jù)進行存儲。字段 CopyOnWriteArrayList#lock 對應 ReentrantLock 類型,用于控制多個線程對底層數(shù)組的修改操作,保證同一時間只有一個線程對底層數(shù)組進行修改。CopyOnWriteArrayList 提供了相應的 CopyOnWriteArrayList#getArrayCopyOnWriteArrayList#setArray 方法用于訪問該字段。

CopyOnWriteArrayList 定義了多個重載的構(gòu)造方法來初始化構(gòu)造一個 CopyOnWriteArrayList 對象,實現(xiàn)上比較簡單。下面重點看一下 CopyOnWriteArrayList 之于 List 接口中聲明的核心方法實現(xiàn),即增(add)、刪(remove)、改(set)、查(get)操作。

首先來看一下 添加元素 的操作,CopyOnWriteArrayList 定義了多個重載版本的 CopyOnWriteArrayList#add 的方法實現(xiàn),包括:

  • CopyOnWriteArrayList#add(E)

  • CopyOnWriteArrayList#add(int, E)

  • CopyOnWriteArrayList#addIfAbsent(E)

  • CopyOnWriteArrayList#addAll(Collection<? extends E>)

  • CopyOnWriteArrayList#addAll(int, Collection<? extends E>)

  • CopyOnWriteArrayList#addAllAbsent

這些方法在實現(xiàn)上思想都是相通的,下面以 CopyOnWriteArrayList#add(E) 方法為例分析往 CopyOnWriteArrayList 中添加元素的運行機制,方法實現(xiàn)如下:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加鎖,保證同一時間只有一個線程修改底層數(shù)組
    lock.lock();
    try {
        // 獲取底層數(shù)組
        Object[] elements = this.getArray();
        int len = elements.length;
        // 復制出一份新的數(shù)組,長度加 1,以容納待添加的元素
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 更新底層數(shù)組
        this.setArray(newElements);
        return true;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

前面我們已經(jīng)提及到 CopyOnWriteArrayList 對于數(shù)據(jù)的修改都是在副本上進行的,上述方法的實現(xiàn)印證了這一點。當往 CopyOnWriteArrayList 中添加一個元素時,方法的執(zhí)行流程如下:

  1. 獲取鎖對象,保證同一時間只有一個線程在執(zhí)行修改操作;

  2. 獲取底層數(shù)組,基于該數(shù)組拷貝出一個新的數(shù)組,新數(shù)組長度加 1;

  3. 追加待添加元素到新數(shù)組的末尾位置,并更新底層數(shù)組;

  4. 釋放鎖對象。

再來看一下 獲取元素 的操作,CopyOnWriteArrayList 僅定義了一個 CopyOnWriteArrayList#get 方法,用于獲取指定下標的元素值,實現(xiàn)如下:

public E get(int index) {
    // 直接返回底層數(shù)組 index 下標的元素
    return this.get(this.getArray(), index);
}

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

實現(xiàn)上比較簡單,這里主要強調(diào)一下弱一致性問題,也就說 get 操作不一定能夠返回最新的值。考慮這樣一個場景,假設有線程 A 和 B,其中 A 調(diào)用 get 方法獲取數(shù)據(jù),B 調(diào)用 add 方法添加數(shù)據(jù),當前數(shù)組長度為 5:

  1. 線程 B 獲取底層數(shù)組 x,然后拷貝出一個長度為 6 的新數(shù)組 y,并將待追加的元素設置到 y[5] 的位置,此時還未更新底層數(shù)組;

  2. 因為讀操作無需獲取鎖,如果此時線程 A 嘗試獲取下標為 5 的元素,則會拋出 IndexOutOfBoundsException,因為此時 A 獲取到的底層數(shù)組還是 x,還沒有更新成 y。

然而,大部分時候這種弱一致性并不會對我們的業(yè)務造成影響,但是我們需要知道其存在,以便在發(fā)生錯誤時快速定位問題。

繼續(xù)來看一下 修改元素 的操作,CopyOnWriteArrayList 同樣僅定義了一個 CopyOnWriteArrayList#set 方法,用于修改指定下標的元素值,實現(xiàn)如下:

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 獲取鎖
    lock.lock();
    try {
        // 獲取底層數(shù)組
        Object[] elements = this.getArray();
        // 獲取數(shù)組 index 下標值
        E oldValue = this.get(elements, index);
        // 待更新的元素值與數(shù)組中指定位置的元素值不同
        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            // 更新 index 位置的元素值
            newElements[index] = element;
            // 更新底層數(shù)組
            this.setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            // 待更新的元素值與數(shù)組中指定位置的元素值相同,無需執(zhí)行更新操作
            this.setArray(elements); // 保證 volatile 寫語義
        }
        return oldValue;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

上述方法的執(zhí)行邏輯如代碼注釋,比較簡單,但是當修改的目標元素值前后未發(fā)生變化時還是會調(diào)用 CopyOnWriteArrayList#setArray 方法更新一下底層數(shù)組,用意何在呢?

代碼中給出的理由是 “Not quite a no-op; ensures volatile write semantics”,也就是說這里的更新操作不完全是一個無意義的操作,可以用來保證 volatile 的寫語義,因為底層數(shù)組 array 是 volatile 類型。要理解其用意,可以參考 《深入理解 java 內(nèi)存模型》一書中的示例(稍作修改):

private int a = 0;
private volatile boolean flag = true;

public void writer() {
    a = 1; // 1
    flag = true; // 2
}

public void reader() {
    if (flag) { // 3
        int i = a; // 4
    }
}

假設線程 A 執(zhí)行 writer 方法之后,線程 B 執(zhí)行 reader 方法。根據(jù) happens-before 原則,這個過程建立的 happens-before 關系為:

  1. 根據(jù)程序次序規(guī)則,1 happens before 2; 3 happens before 4。

  2. 根據(jù) volatile 規(guī)則,2 happens before 3。

  3. 根據(jù) happens before 的傳遞性規(guī)則,1 happens before 4。

這樣就能保證線程 B 在 reader 方法中讀取 a 變量時能夠看見線程 A 在 writer 方法中對 a 的修改,即使在 writer 方法中對變量 flag 的修改同樣看似多余。

最后來看一下 刪除元素 的操作,CopyOnWriteArrayList 針對刪除操作定義了多個重載版本的 CopyOnWriteArrayList#remove 的方法實現(xiàn),包括:

  • CopyOnWriteArrayList#remove(int)

  • CopyOnWriteArrayList#remove(java.lang.Object)

  • CopyOnWriteArrayList#removeAll

  • CopyOnWriteArrayList#removeIf

下面以 CopyOnWriteArrayList#remove(int) 方法為例分析從 CopyOnWriteArrayList 中刪除元素的運行機制,方法實現(xiàn)如下:

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    // 獲取鎖
    lock.lock();
    try {
        // 獲取底層數(shù)組
        Object[] elements = this.getArray();
        int len = elements.length;
        // 獲取指定下標元素
        E oldValue = this.get(elements, index);
        int numMoved = len - index - 1;
        // 如果是刪除最后一個元素,則直接 copy 即可,無需移動
        if (numMoved == 0) {
            this.setArray(Arrays.copyOf(elements, len - 1));
        } else {
            // 否則,分兩次進行拷貝,去掉原 index 下標的元素
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index, numMoved);
            this.setArray(newElements);
        }
        return oldValue;
    } finally {
        // 釋放鎖
        lock.unlock();
    }
}

刪除的邏輯本質(zhì)上還是在副本上進行,如上述代碼注釋,其過程與前面分析的操作類似。

除了上面分析的核心操作方法,CopyOnWriteArrayList 還實現(xiàn)了 CopyOnWriteArrayList#iterator 方法返回一個迭代器,實現(xiàn)如下:

public Iterator<E> iterator() {
    return new COWIterator<E>(this.getArray(), 0);
}

關于 COWIterator 的實現(xiàn)不再繼續(xù)深入,但是需要知曉的一點是,COWIterator 不支持在迭代過程中修改 CopyOnWriteArrayList 中的元素,對應的 COWIterator#remove、COWIterator#setCOWIterator#add 方法均直接拋出 UnsupportedOperationException 異常。此外,方法 CopyOnWriteArrayList#iterator 返回的是一個弱一致性迭代器,即在迭代期間,其它線程對于底層數(shù)組的修改并不會被迭代器看見。

到此,關于“JUC的CopyOnWriteArrayList怎么理解”的學習就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI