聊一聊 CopyOnWriteArraySet 的迭代删除
上周在工程中涉及到一个清理 Set
集合的操作,将满足设定条件的项从 Set
中删除掉。简化版本代码如下:
1 | public static void main(String[] args) { |
这个看起来是个很常规的问题,没有验证就直接发了线下环境,然后就收到了业务方反馈的服务无法正常使用的问题了。
问题现象
先来看下上述代码所抛出的异常:
1 | Exception in thread "main" java.lang.UnsupportedOperationException |
关于 UnsupportedOperationException
这个异常没有什么好说的,在集合操作中经常出现,网上也有很多关于这个异常的说明,这里不再赘述。这里我比较关注的是,我使用的是 CopyOnWriteArraySet
,迭代器也是 sets
的,但是异常中居然出现了CopyOnWriteArrayList
,查看了 CopyOnWriteArraySet
的类继承关系,和 CopyOnWriteArrayList
也没啥关系。
排查&结果
通过查看了 CopyOnWriteArraySet
的代码,发现 CopyOnWriteArraySet
内部其实是持有了一个 CopyOnWriteArrayList
的对象实例,其内部的所有操作都是基于 CopyOnWriteArrayList
这个对象来进行的。
1 | public class CopyOnWriteArraySet<E> extends AbstractSet<E> |
关于 CopyOnWriteArrayList 的操作
写操作
在 CopyOnWriteArrayList
里处理写操作(包括 add、remove、set
等)是先将原始的数据通过 JDK1.6
的 Arrays.copyof()
来生成一份新的数组。add
的代码如下:
1 | public boolean add(E e) { |
后续的操作都是在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上(因为要保证写的一致性,这里要对各种写操作要加一把锁,JDK1.6 在这里用了重入锁),
读操作
读的时候就是在引用的当前对象上进行读(包括 get,iterator
等),不存在加锁和阻塞,针对 iterator
使用了一个叫 COWIterator
的简化版迭代器,因为不支持写操作,当获取 CopyOnWriteArrayList
的迭代器时,是将迭代器里的数据引用指向当前引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全。
结论
因为 CopyOnWriteArraySet
的内部操作都是基于 CopyOnWriteArrayList
的,从异常来看:
1 | java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178) |
COWIterator
是 CopyOnWriteArrayList
内部提供的一个简化版的迭代器。所以异常里面出现这个就理所应当了。在来看下 COWIterator
这里简化版的迭代器的 remove
方法:
1 | /** |
这里实际上是直接就会抛出异常的,另外这里在多补充一个关于 HashSet
的迭代器移除,HashSet
其实内部是持有的 HashMap
实例,因此它的迭代器是 HashMap
内部提供的 HashIterator
:
1 | public final void remove() { |
这里其实也可以看到,在对非安全的集合做 remove
操作时会经常遇到的 ConcurrentModificationException
这个异常。
聊一聊 CopyOnWriteArraySet 的迭代删除
http://www.glmapper.com/2020/03/16/java/java-base-iterator-of-set/