91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

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

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

CopyOnWriteArrayList中的隱藏的知識點有哪些

發布時間:2021-10-26 11:36:16 來源:億速云 閱讀:110 作者:iii 欄目:編程語言

本篇內容主要講解“CopyOnWriteArrayList中的隱藏的知識點有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“CopyOnWriteArrayList中的隱藏的知識點有哪些”吧!

線程安全 List

在 Java 中,線程安全的 List 不止一個,除了今天的主角 CopyOnWriteArrayList 之外,還有 Vector 類和 SynchronizedList 類,它們都是線程安全的 List 集合。在介紹 CopyOnWriteArrayList 之前,先簡單介紹下另外兩個。

如果你嘗試你查看它們的源碼,你會發現有點不對頭,并發集合不都是在 java.util.concurrent 包中嘛,為什么Vector 類和 SynchronizedList 類 這兩個是在 java.util 包里呢?

確實是這樣的,這兩個線程安全的 List 和線程安全的 HashTable 是一樣的,都是比較簡單粗暴的實現方式,直接方法上增加 synchronized 關鍵字實現的,而且不管增刪改查,統統加上,即使是 get 方法也不例外,沒錯,就是這么粗暴。

Vector 類的 get 方法:

// Vector 中的 get 操作添加了 synchronized
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

SynchronizedList 類的 ge t 方法:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

同學不妨思考一下,其實在 get 方法上添加同步機制也是有原因的,雖然降低了效率,但是可以讓寫入的數據立即可以被查詢到,這也保證了數據的強一致性。另外上面關于 synchronized 簡單粗暴的描述也是不夠準確的,因為在高版本的 JDK 中,synchronized 已經可以根據運行時情況,自動調整鎖的粒度,后面介紹 CopyOnWriteArrayList 時會再次講到。

CopyOnWriteArrayList

在 JDK 并發包中,目前關于 List 的并發集合,只有 CopyOnWriteArrayList 一個。上面簡單介紹了 Vector 和 SynchronizdList 的粗暴實現,既然還有 CopyOnWriteArrayList,那么它一定是和上面兩種是有區別的,作為唯一的并發 List,它有什么不同呢?

在探究 CopyOnWriteArrayList 的實現之前,我們不妨先思考一下,如果是你,你會怎么來實現一個線程安全的 List。

  1. 并發讀寫時該怎么保證線程安全呢?

  2. 數據要保證強一致性嗎?數據讀寫更新后是否立刻體現?

  3. 初始化和擴容時容量給多少呢?

  4. 遍歷時要不要保證數據的一致性呢?需要引入 Fail-Fast 機制嗎?

通過類名我們大致可以猜測到 CopyOnWriteArrayList 類的實現思路:Copy-On-Write, 也就是寫時復制策略;末尾的 ArrayList 表示數據存放在一個數組里。在對元素進行增刪改時,先把現有的數據數組拷貝一份,然后增刪改都在這個拷貝數組上進行,操作完成后再把原有的數據數組替換成新數組。這樣就完成了更新操作。

但是這種寫入時復制的方式必定會有一個問題,因為每次更新都是用一個新數組替換掉老的數組,如果不巧在更新時有一個線程正在讀取數據,那么讀取到的就是老數組中的老數據。其實這也是讀寫分離的思想,放棄數據的強一致性來換取性能的提升

分析源碼 ( JDK8 )

上面已經說了,CopyOnWriteArrayList 的思想是寫時復制讀寫分離,它的內部維護著一個使用 volatile 修飾的數組,用來存放元素數據。

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

CopyOnWriteArrayList 類中方法很多,這里不會一一介紹,下面會分析其中的幾個常用的方法,這幾個方法理解后基本就可以掌握 CopyOnWriteArrayList 的實現原理。

構造函數

CopyOnWriteArrayList 的構造函數一共有三個,一個是無參構造,直接初始化數組長度為0;另外兩個傳入一個集合或者數組作為參數,然后會把集合或者數組中的元素直接提取出來賦值給 CopyOnWriteArrayList 內部維護的數組。

// 直接初始化一個長度為 0 的數組
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
// 傳入一個集合,提取集合中的元素賦值到 CopyOnWriteArrayList 數組
public CopyOnWriteArrayList(Collection<!--? extends E--> c) {
    Object[] es;
    if (c.getClass() == CopyOnWriteArrayList.class)
        es = ((CopyOnWriteArrayList<!--?-->)c).getArray();
    else {
        es = c.toArray();
        if (c.getClass() != java.util.ArrayList.class)
            es = Arrays.copyOf(es, es.length, Object[].class);
    }
    setArray(es);
}
// 傳入一個數組,數組元素提取后賦值到 CopyOnWriteArrayList 數組
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

構造函數是實例創建時調用的,沒有線程安全問題,所以構造方法都是簡單的賦值操作,沒有特殊的邏輯處理。

新增元素

元素新增根據入參的不同有好幾個,但是原理都是一樣的,所以下面只貼出了 add(E e ) 的實現方式,是通過一個 ReentrantLock 鎖保證線程安全的。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
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); // 拷貝一個數據數組,長度+1
        newElements[len] = e; // 加入新元素
        setArray(newElements); // 用新數組替換掉老數組
        return true;
    } finally {
        lock.unlock();
    }
}

具體步驟:

  1. 加鎖,獲取目前的數據數組開始操作(加鎖保證了同一時刻只有一個線程進行增加/刪除/修改操作)。

  2. 拷貝目前的數據數組,且長度增加一。

  3. 新數組中放入新的元素。

  4. 用新數組替換掉老的數組。

  5. finally 釋放鎖。

由于每次 add 時容量只增加了1,所以每次增加時都要創建新的數組進行數據復制,操作完成后再替換掉老的數據,這必然會降低數據新增時候的性能。下面通過一個簡單的例子測試 CopyOnWriteArrayList 、Vector、ArrayList 的新增和查詢性能。

public static void main(String[] args) {
    CopyOnWriteArrayList<object> copyOnWriteArrayList = new CopyOnWriteArrayList&lt;&gt;();
    Vector vector = new Vector&lt;&gt;();
    ArrayList arrayList = new ArrayList();

    add(copyOnWriteArrayList);
    add(vector);
    add(arrayList);

    get(copyOnWriteArrayList);
    get(vector);
    get(arrayList);
}
public static void add(List list) {
    long start = System.currentTimeMillis();
    for (int i = 0; i &lt; 100000; i++) {
        list.add(i);
    }
    long end = System.currentTimeMillis();
    System.out.println(list.getClass().getName() + ".size=" + list.size() + ",add耗時:" + (end - start) + "ms");
}
public static void get(List list) {
    long start = System.currentTimeMillis();
    for (int i = 0; i &lt; list.size(); i++) {
        Object object = list.get(i);
    }
    long end = System.currentTimeMillis();
    System.out.println(list.getClass().getName() + ".size=" + list.size() + ",get耗時:" + (end - start) + "ms");
}

從測得的結果中可以看到 CopyOnWriteArrayList 的新增耗時最久,其次是加鎖的 Vector(Vector 的擴容默認是兩倍)。而在獲取時最快的是線程不安全的 ArrayList,其次是 CopyOnWriteArrayList,而 Vector 因為 Get 時加鎖,性能最低。

java.util.concurrent.CopyOnWriteArrayList.size=100000,add耗時:2756ms
java.util.Vector.size=100000,add耗時:4ms
java.util.ArrayList.size=100000,add耗時:3ms
java.util.concurrent.CopyOnWriteArrayList.size=100000,get耗時:4ms
java.util.Vector.size=100000,get耗時:5ms
java.util.ArrayList.size=100000,get耗時:2ms

修改元素

修改元素和新增元素的思想是一致的,通過 ReentrantLock 鎖保證線程安全性,實現代碼也比較簡單,本來不準備寫進來的,但是在看源碼時發現一個非常有意思的地方,看下面的代碼。

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock(); //加鎖
    try {
        Object[] elements = getArray(); // 獲取老數組
        E oldValue = get(elements, index); // 獲取指定位置元素

        if (oldValue != element) { // 新老元素是否相等,不相等
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len); // 復制老數組
            newElements[index] = element; // 指定位置賦新值
            setArray(newElements); // 替換掉老數組
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);  // 有意思的地方來了
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

通過源碼可以看到在修改元素前會先比較修改前后的值是否相等,而在相等的情況下,依舊 setArray(elements); 這就很奇妙了,到底是為什么呢?想了解其中的原因需要了解下 volatile 的特殊作用,通過下面這個代碼例子說明。

// initial conditions
int nonVolatileField = 0;
CopyOnWriteArrayList<string> list = /* a single String */

// Thread 1
nonVolatileField = 1;                 // (1)
list.set(0, "x");                     // (2)

// Thread 2
String s = list.get(0);               // (3)
if (s == "x") {
    int localVar = nonVolatileField;  // (4)
}
// 例子來自:https://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist

要想理解例子中的特殊之處,首先你要知道 volatile 可以防止指令重排,其次要了解 happens-before 機制。說簡單點就是它們可以保證代碼的執行前后順序。

比如上面例子中的代碼,1 會在 2 之前執行,3 會在 4 之前執行,這都沒有疑問。還有一條是 volatile 修飾的屬性寫會在讀之前執行,所以 2會在 3 之前執行。而執行順序還存在傳遞性。所以最終 1 會在 4 之前執行。這樣 4 獲取到的值就是步驟 1 為 nonVolatileField 賦的值。如果 CopyOnWriteArrayList 中的 set 方法內沒有為相同的值進行 setArray,那么上面說的這些就都不存在了。

刪除元素

remove 刪除元素方法一共有三個,這里只看public E remove(int index) 方法,原理都是類似的。

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 加鎖
    try {
        Object[] elements = getArray(); // 獲取數據數組
        int len = elements.length;
        E oldValue = get(elements, index); // 獲取要刪除的元素
        int numMoved = len - index - 1;
        if (numMoved == 0) // 是否末尾
            setArray(Arrays.copyOf(elements, len - 1)); // 數據數組減去末尾元素
        else {
            Object[] newElements = new Object[len - 1]; // 把要刪除的數據的前后元素分別拷貝到新數組
            System.arraycopy(elements, 0, newElements, 0, index); 
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements); // 使用新數組替換老數組
        }
        return oldValue;
    } finally {
        lock.unlock(); // 解鎖
    }
}

代碼還是很簡單的,使用 ReentrantLock 獨占鎖保證操作的線程安全性,然后使用刪除元素后的剩余數組元素拷貝到新數組,使用新數組替換老數組完成元素刪除,最后釋放鎖返回。

獲取元素

獲取下標為 index 的元素,如果元素不存在,會拋出IndexOutOfBoundsException 異常。

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

首先看到這里是沒有任何的加鎖操作的,而獲取指定位置的元素又分為了兩個步驟:

  1. getArray() 獲取數據數組。

  2. get(Object[] a, int index) 返回指定位置的元素。

很有可能在第一步執行完成之后,步驟二執行之前,有線程對數組進行了更新操作。通過上面的分析我們知道更新會生成一個新的數組,而我們第一步已經獲取了老數組,所以我們在進行 get 時依舊在老數組上進行,也就是說另一個線程的更新結果沒有對我們的本次 get 生效。這也是上面提到的弱一致性問題。

迭代器的弱一致性

List<string> list = new CopyOnWriteArrayList&lt;&gt;();
list.add("www.wdbyte.com");
list.add("未讀代碼");

Iterator<string> iterator = list.iterator();
list.add("java");
while (iterator.hasNext()) {
    String next = iterator.next();
    System.out.println(next);
}

現在 List 中添加了元素 www.wdbyte.com未讀代碼,在拿到迭代器對象后,又添加了新元素 java ,可以看到遍歷的結果沒有報錯也沒有輸出 java 。也就是說拿到迭代器對象后,元素的更新不可見。

www.wdbyte.com
未讀代碼

這是為什么呢?要先從CopyOnWriteArrayList 的 iterator() 方法的實現看起。

public Iterator<e> iterator() {
    return new COWIterator<e>(getArray(), 0);
}
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;
    }
......

可以看到在獲取迭代器時,先 getArray() 拿到了數據數組 然后傳入到 COWIterator 構造器中,接著賦值給了COWIterator 中的 snapshot 屬性,結合上面的分析結果,可以知道每次更新都會產生新的數組,而這里使用的依舊是老數組,所以更新操作不可見,也就是上面多次提到的弱一致性

新版變化

上面的源碼分析都是基于 JDK 8 進行的。寫文章時順便看了下新版的實現方式有沒有變化,還真的有挺大的改變,主要體現在加鎖的方式上,或許是因為 JVM 后來引入了 synchronized 鎖升級策略,讓 synchronized 性能有了不少提升,所以用了 synchronized 鎖替換了老的 ReentrantLock 鎖。

新增:

public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

修改:

public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
        }
        // Ensure volatile write semantics even when oldvalue == element
        setArray(es);
        return oldValue;
    }
}

總結

通過上面的分析,得到下面幾點關于 CopyOnWriteArrayList 的總結。

  1. CopyOnWriteArrayList 采用讀寫分離,寫時復制方式實現線程安全,具有弱一致性。

  2. CopyOnWriteArrayList 因為每次寫入時都要擴容復制數組,寫入性能不佳。

  3. CopyOnWriteArrayList 在修改元素時,為了保證 volatile 語義,即使元素沒有任何變化也會重新賦值,

  4. 在高版 JDK 中,得益于 synchronized 鎖升級策略, CopyOnWriteArrayList 的加鎖方式采用了 synchronized。

到此,相信大家對“CopyOnWriteArrayList中的隱藏的知識點有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

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

AI

都匀市| 长丰县| 白山市| 连江县| 万盛区| 阳新县| 阆中市| 崇左市| 莆田市| 马公市| 康乐县| 襄城县| 同心县| 驻马店市| 靖宇县| 云阳县| 阿拉善左旗| 鄂州市| 留坝县| 临城县| 同江市| 濮阳县| 凤凰县| 小金县| 桂东县| 许昌县| 南召县| 黑龙江省| 陈巴尔虎旗| 鞍山市| 五大连池市| 常宁市| 高平市| 尉犁县| 新兴县| 邹城市| 日照市| 普兰店市| 宜良县| 桦南县| 南江县|