为何Java HashSet未触发ConcurrentModificationException,ArrayList却可以?
为什么HashSet没触发ConcurrentModificationException,ArrayList却触发了?
核心原因在于两个集合的fail-fast迭代器触发条件、以及结构修改的判定逻辑存在差异,具体可以从以下几点分析:
1. 结构修改的判定标准不同
- ArrayList:只要调用
add()方法,无论元素是否重复,都会直接修改底层数组(扩容或追加元素),同时modCount(记录结构修改次数的变量)一定会自增。哪怕添加重复元素,也会被视为一次结构修改。 - HashSet:底层依赖
HashMap实现,add()方法本质是调用HashMap.put()。只有当添加的元素是集合中不存在的(即HashMap中没有对应key),才会修改集合结构,modCount才会自增;如果添加的是重复元素,put()不会改变集合结构,modCount保持不变。
如果你的测试代码中,第一个线程持续添加的是重复元素,那HashSet的modCount不会变化,迭代器自然检测不到并发修改,不会抛出异常。而ArrayList不管元素是否重复,每次add()都会让modCount自增,很容易触发迭代器的检查逻辑。
2. 迭代器的检查时机与逻辑细节差异
虽然两者的迭代器都基于fail-fast机制,但检查的触发场景有细微区别:
- ArrayList的迭代器(Itr):在每次调用
next()方法时,都会直接对比modCount和expectedModCount,一旦不一致立即抛出异常。而打印集合时的toString()方法会触发遍历,每一次元素迭代都会执行这个检查,只要另一个线程在遍历过程中修改了modCount,几乎一定会触发异常。 - HashSet的迭代器(基于HashMap的HashIterator):同样会在
nextNode()方法中检查modCount,但HashMap的遍历是基于链表/红黑树的节点跳转,加上哈希表的扩容机制,可能存在线程调度巧合:比如迭代器刚完成一次检查,另一个线程的修改发生在两次检查之间,或者修改后迭代器刚好遍历到末尾,没机会执行下一次检查。不过这种情况概率较低,更核心的原因还是第一条的结构修改判定差异。
3. 额外说明:fail-fast机制的局限性
需要注意的是,ConcurrentModificationException并不是一定会触发的,它只是fail-fast机制提供的并发修改检测提示,不保证所有并发修改场景都能被捕获。无论是ArrayList还是HashSet,都不是线程安全的集合,在多线程环境下直接修改都可能导致数据错乱,只是ArrayList更容易触发这个异常而已。
内容的提问来源于stack exchange,提问作者CS1999
相关产品推荐
相关产品推荐

