你了解 ConcurrentModificationException 吗?

2023-12-08 08:58

本文主要是介绍你了解 ConcurrentModificationException 吗?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见100个问题搞定Java并发

正文

WHY

JDK 中很多容器类在面对复合操作会存在问题,无论在直接迭代还是在 Java 5.0 引人的 for - each 循环语法中,对容器类进行迭代的标准方式都是使用 Iterator ,然而,如果有其他线程并发地修改容器,那么即使是使用迭代器也无法避免在迭代期间对容器加锁。

在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是“及时失败”( fail - fast )的。

这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个 ConcurrentModificationException 异常。

WHAT

这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示器。

它们采用的实现方式是,将计数器的变化与容器关联起来:

如果在迭代期间计数器被修改,那么 hasNext 或 next 将抛出 ConcurrentModificationException

然而,这种检査是在没有同步的情况下进行的,因此可能会看到失效的计数值,而迭代器可能并没有意识到已经发生了修改。

这是一种设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。

单线程删除

在单线程代码中也可能抛出 ConcurrentModificationException 异常。

当对象直接从容器中删除而不是通过 Iterator.remove 来删除时,就容易抛出这个异常。

错误示例

package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test1 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("2".equals(s)) {list.remove(s);}}System.out.println(list);}
}
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at java.util.ArrayList$Itr.next(ArrayList.java:859)at com.shockang.study.java.concurrent.iterator.Test1.main(Test1.java:11)

正确示例

示例 1
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test2 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("1".equals(s)) {list.remove(s);}}System.out.println(list);}
}
[2]
示例 2
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test3 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");for (String s : list) {if ("2".equals(s)) {list.remove(s);break;}}System.out.println(list);}
}
[1]
示例 3
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class Test4 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");Iterator<String> it = list.iterator();while (it.hasNext()) {String next = it.next();if ("2".equals(next)) {it.remove();}}System.out.println(list);}
}
[1]
示例 4
package com.shockang.study.java.concurrent.iterator;import java.util.ArrayList;
import java.util.List;public class Test5 {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("1");list.add("2");list.removeIf("2"::equals);System.out.println(list);}
}
[1]

迭代期间加锁

然而,有时候开发人员并不希望在迭代期间对容器加锁。

例如,某些线程在可以访问容器之前,必须等待迭代过程结束,如果容器的规模很大,或者在每个元素上执行操作的时间很长,那么这些线程将长时间等待,同时也可能存在死锁的风险。

关于死锁请参考我的这篇博客——死锁、活锁和饥饿是什么意思?

即使不存在饥饿或者死锁等风险,长时间地对容器加锁也会降低程序的可伸缩性。

持有锁的时间越长,那么在锁上的竞争就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和 CPU 的利用率。

如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器,并在副本上进行迭代。

参考我的这篇博客——一篇文章搞懂 CopyOnWriteArrayList

由于副本被封闭在线程内,因此其他线程不会在选代期间对其进行修改,这样就避免了抛出 ConcurrentModificationException (在克隆过程中仍然需要对容器加锁)。

在克隆容器时存在显著的性能开销。

这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。

隐蔽的迭代器

虽然加锁可以防止迭代器抛出 ConcurrentModificationException ,但你必须要记住在所有对共享容器进行迭代的地方都需要加锁。

实际情况要更加复杂,因为在某些情况下,迭代器会隐藏起来。

容器的 hashCodeequals 等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。

同样, containsAllremoveAllretainAll 等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。

所有这些间接的迭代操作都可能抛出 ConcurrentModificationException 。

这篇关于你了解 ConcurrentModificationException 吗?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/469266

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

速了解MySQL 数据库不同存储引擎

快速了解MySQL 数据库不同存储引擎 MySQL 提供了多种存储引擎,每种存储引擎都有其特定的特性和适用场景。了解这些存储引擎的特性,有助于在设计数据库时做出合理的选择。以下是 MySQL 中几种常用存储引擎的详细介绍。 1. InnoDB 特点: 事务支持:InnoDB 是一个支持 ACID(原子性、一致性、隔离性、持久性)事务的存储引擎。行级锁:使用行级锁来提高并发性,减少锁竞争

PHP: 深入了解一致性哈希

前言 随着memcache、redis以及其它一些内存K/V数据库的流行,一致性哈希也越来越被开发者所了解。因为这些内存K/V数据库大多不提供分布式支持(本文以redis为例),所以如果要提供多台redis server来提供服务的话,就需要解决如何将数据分散到redis server,并且在增减redis server时如何最大化的不令数据重新分布,这将是本文讨论的范畴。 取模算法 取模运

Weex入门教程之1,了解Weex

【资料合集】Weex Conf回顾集锦:讲义PDF+活动视频! PDF分享:链接:http://pan.baidu.com/s/1hr8RniG 密码:fa3j 官方教程:https://weex-project.io/cn/v-0.10/guide/index.html 用意 主要是介绍Weex,并未涉及开发方面,好让我们开始开发之前充分地了解Weex到底是个什么。 以下描述主要摘取于

Java了解相对较多!

我是对Java了解相对较多,而对C#则是因工作需要才去看了一下,C#跟Java在语法上非常相似,而最初让我比较困惑的就是委托、事件部分,相信大多数初学者也有类似的困惑。经过跟Java的对比学习,发现这其实跟Java的监听、事件是等同的,只是表述上不同罢了。   委托+事件是观察者模式的一个典型例子,所谓的委托其实就是观察者,它会关心某种事件,一旦这种事件被触发,这个观察者就会行动。   下

使用WebP解决网站加载速度问题,这些细节你需要了解

说到网页的图片格式,大家最常想到的可能是JPEG、PNG,毕竟这些老牌格式陪伴我们这么多年。然而,近几年,有一个格式悄悄崭露头角,那就是WebP。很多人可能听说过,但到底它好在哪?你的网站或者项目是不是也应该用WebP呢?别着急,今天咱们就来好好聊聊WebP这个图片格式的前世今生,以及它值不值得你花时间去用。 为什么会有WebP? 你有没有遇到过这样的情况?网页加载特别慢,尤其是那

初步了解VTK装配体

VTK还不太了解,根据资料, vtk.vtkAssembly 是 VTK库中的一个重要类,允许通过将多个vtkActor对象组合在一起来创建复杂的3D模型。 import vtkimport mathfrom vtk.util.colors import *filenames = ["cylinder.stl","sphere.stl","torus.stl"]dt = 1.0renW

Post-Training有多重要?一文带你了解全部细节

1. 简介 随着LLM学界和工业界日新月异的发展,不仅预训练所用的算力和数据正在疯狂内卷,后训练(post-training)的对齐和微调方法也在不断更新。InstructGPT、WebGPT等较早发布的模型使用标准RLHF方法,其中的数据管理风格和规模似乎已经过时。近来,Meta、谷歌和英伟达等AI巨头纷纷发布开源模型,附带发布详尽的论文或报告,包括Llama 3.1、Nemotron 340

了解elementUI的底层源码, 进行二次开发

Element UI 是一个基于 Vue.js 的桌面端组件库,广泛用于构建美观、交互友好的用户界面。要深入理解 Element UI 的底层源码并进行二次开发,你需要掌握以下几个关键点: Vue.js 原理 Element UI 是基于 Vue.js 构建的,因此首先需要熟悉 Vue.js 的核心概念和机制,包括: ● 组件系统:Vue.js 的组件化思想,如何定义组件、使用组件、传递属性和事

【JavaScript】在循环体中了解定时器工作机制

for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i);}, 1000);}console.log(i);   如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?会有下面两种答案: A. :5 -> 5 -> 5 ->