聊一聊 CopyOnWriteArraySet 的迭代删除

上周在工程中涉及到一个清理 Set 集合的操作,将满足设定条件的项从 Set 中删除掉。简化版本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

这个看起来是个很常规的问题,没有验证就直接发了线下环境,然后就收到了业务方反馈的服务无法正常使用的问题了。

问题现象

先来看下上述代码所抛出的异常:

1
2
3
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 也没啥关系。

排查&结果

通过查看了 CopyOnWriteArraySet 的代码,发现 CopyOnWriteArraySet 内部其实是持有了一个 CopyOnWriteArrayList 的对象实例,其内部的所有操作都是基于 CopyOnWriteArrayList 这个对象来进行的。

1
2
3
4
5
6
7
8
9
10
11
12
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 的操作

写操作

CopyOnWriteArrayList 里处理写操作(包括 add、remove、set 等)是先将原始的数据通过 JDK1.6Arrays.copyof() 来生成一份新的数组。add 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 在这里用了重入锁),

读操作

读的时候就是在引用的当前对象上进行读(包括 get,iterator 等),不存在加锁和阻塞,针对 iterator 使用了一个叫 COWIterator 的简化版迭代器,因为不支持写操作,当获取 CopyOnWriteArrayList 的迭代器时,是将迭代器里的数据引用指向当前引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全。

结论

因为 CopyOnWriteArraySet 的内部操作都是基于 CopyOnWriteArrayList 的,从异常来看:

1
java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

COWIteratorCopyOnWriteArrayList 内部提供的一个简化版的迭代器。所以异常里面出现这个就理所应当了。在来看下 COWIterator 这里简化版的迭代器的 remove 方法:

1
2
3
4
5
6
7
8
/**
* 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

1
2
3
4
5
6
7
8
9
10
11
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 这个异常。

聊一聊 CopyOnWriteArraySet 的迭代删除

http://www.glmapper.com/2020/03/16/java/java-base-iterator-of-set/

作者

卫恒

发布于

2020-03-16

更新于

2022-04-23

许可协议

评论