聊一聊 CopyOnWriteArraySet 的迭代刪除

上週在工程中涉及到一個清理 Set 集合的操做,將知足設定條件的項從 Set 中刪除掉。簡化版本代碼以下:java

public static void main(String[] args) {
    Set<String> sets = new CopyOnWriteArraySet<>();
    sets.add("1");
    sets.add("3");
    sets.add("3");
    sets.add("4");
    Iterator<String> iterator = sets.iterator();
    while (iterator.hasNext()){
        iterator.remove();
    }
    System.out.println(sets);
    }
複製代碼

這個看起來是個很常規的問題,沒有驗證就直接發了線下環境,而後就收到了業務方反饋的服務沒法正常使用的問題了。數組

問題現象

先來看下上述代碼所拋出的異常:安全

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)
	at com.glmapper.bridge.boot.TestMain.main(TestMain.java:21)
複製代碼

關於 UnsupportedOperationException 這個異常沒有什麼好說的,在集合操做中常常出現,網上也有不少關於這個異常的說明,這裏再也不贅述。這裏我比較關注的是,我使用的是 CopyOnWriteArraySet,迭代器也是 sets 的,可是異常中竟然出現了 CopyOnWriteArrayList,查看了 CopyOnWriteArraySet 的類繼承關係,和 CopyOnWriteArrayList 也沒啥關係。bash

排查&結果

經過查看了 CopyOnWriteArraySet 的代碼,發現 CopyOnWriteArraySet 內部實際上是持有了一個 CopyOnWriteArrayList 的對象實例,其內部的全部操做都是基於 CopyOnWriteArrayList 這個對象來進行的。app

public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
    // 省略其餘代碼
    private final CopyOnWriteArrayList<E> al;
    /** * Creates an empty set. */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
    // 省略其餘代碼
}
複製代碼

關於 CopyOnWriteArrayList 的操做

寫操做this

在 CopyOnWriteArrayList 裏處理寫操做(包括 add、remove、set 等)是先將原始的數據經過 JDK1.6 的 Arrays.copyof() 來生成一份新的數組。add 的代碼以下:spa

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();
    }
}
複製代碼

後續的操做都是在新的數據對象上進行寫,寫完後再將原來的引用指向到當前這個數據對象,這樣保證了每次寫都是在新的對象上(由於要保證寫的一致性,這裏要對各類寫操做要加一把鎖,JDK1.6 在這裏用了重入鎖),code

讀操做對象

讀的時候就是在引用的當前對象上進行讀(包括 get,iterator 等),不存在加鎖和阻塞,針對 iterator 使用了一個叫 COWIterator 的簡化版迭代器,由於不支持寫操做,當獲取 CopyOnWriteArrayList 的迭代器時,是將迭代器裏的數據引用指向當前引用指向的數據對象,不管將來發生什麼寫操做,都不會再更改迭代器裏的數據對象引用,因此迭代器也很安全。繼承

結論

由於 CopyOnWriteArraySet 的內部操做都是基於 CopyOnWriteArrayList 的,從異常來看:

java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)
複製代碼

COWIterator 是 CopyOnWriteArrayList 內部提供的一個簡化版的迭代器。因此異常裏面出現這個就理所應當了。在來看下 COWIterator 這裏簡化版的迭代器的 remove 方法:

/** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code remove} * is not supported by this iterator. */
public void remove() {
    throw new UnsupportedOperationException();
}
複製代碼

這裏其實是直接就會拋出異常的,另外這裏在多補充一個關於 HashSet 的迭代器移除,HashSet 其實內部是持有的 HashMap 實例,所以它的迭代器是 HashMap 內部提供的 HashIterator:

public final void remove() {
    Node<K,V> p = current;
    if (p == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount;
}
複製代碼

這裏其實也能夠看到,在對非安全的集合作 remove 操做時會常常遇到的 ConcurrentModificationException 這個異常。

相關文章
相關標籤/搜索