ConcurrentModificationException日志关键字报警引发的思考
本文将记录和分析日志中的ConcurrentModificationException关键字报警,还有一些我的思考,希望对大家有帮助。
一、背景
近期,在日常的日志关键字报警分析时,发现我负责的一个电商核心系统在某时段存在较多ConcurrentModificationException异常日志,遂进行分析和改进,下面是我的一些思考。
1.1 系统架构
一直以来,无状态的服务都被当作分布式服务设计的最佳实践。因为无状态的服务对于扩展性和运维方面有着得天独厚的优势,可以随意地增加和减少节点。本系统的整体架构可以认为是由一个MQ应用、一个RPC应用和底层存储组成。
RPC应用是无状态服务,对外提供常用的查询和操作接口;采用双机房部署,每个机房10*8C16G;
MQ应用是无状态服务,负责消费MQ消息,在消费过程中会调用该RPC应用提供方法;采用双机房部署,每个机房5*8C16G;
底层存储用的是数据库集群和缓存集群,大概如图所示:

1.2 关键代码
MyRpcService 对外提供RPC服务,getList 方法可以根据入参中的状态进行查询,由于业务需要,需要对入参的状态进行排序,实现部分关键代码如下:
public class MyRpcServiceImpl implements MyRpcService{
@Override
public BaseResult getList(ListParam listParam) {
BaseResult baseResult = new BaseResult();
List<Integer> states = listParam.getStateList();
// 省略大段代码
KeyUtil.getKeyString(states);
// 省略大段代码
baseResult.setSuccess(true);
return baseResult;
}
}
KeyUtil 是一个工具类,getKeyString 方法对入参的itemList进行排序使用的是Java集合框架内置的sort 方法,代码如下:
public class KeyUtil {
public static String getKeyString(List<Integer> itemList) {
String result = "";
//省略代码
Collections.sort(itemList);
//省略代码
return result;
}
}
MyMqConsumer是MQ消费者,负责监听消息进行消费。在消费逻辑中,会调用MyRpcService的getList() 方法进行状态查询,因为查询的状态是固定的,所以在Consumer类中定义了static final 类型的stateList ,关键代码如下:
public class MyMqConsumer implements MessageListener{
public static final List<Integer> stateList = Stream.of(1).collect(Collectors.toList());
@Resource
private MyRpcService myRpcService;
@Override
public void onMessage(List<Message> messageList) {
// 省略代码
for (Message message : messageList) {
// 省略其他代码
ListParam listParam = new ListParam();
listParam.setStateList(stateList);
BaseResult result = myRpcService.getList(listParam);
// 省略其他代码
}
}
}
二、 原因分析
看了上面的系统架构和关键代码,不知道你有没有发现问题?可以先抛开设计和代码实现方面的问题不谈,只看这样的代码能不能正常执行,得到正确的业务结果。
既然这么问了,当然会有问题:在高并发环境下,MQ应用在消费消息时,调用RPC服务查询时可能会抛出异常,从而触发MQ异常重试,至于对业务有没有影响,得具体问题具体分析了。
ERROR 执行流程时出错
java.util.ConcurrentModificationException:null
at java.util.ArrayList.forEach(ArrayList.java:1260)~[:?1.8.0_192]
at com.shangguan.test.util.KeyUtil.getKeyString(KeyUtil.java:10)
...
2.1 分析1-ArrayList源码
从日志中可以看到,ConcurrentModificationException是java.util.ArrayList类里面的forEach方法抛出来的,源码如下:
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
在该方法中,内部会维护一个expectedModCount变量,赋值为modCount,在每次迭代过程中,迭代器会检查expectedModCount是否等于当前的modCount。如果不等,说明在迭代过程中ArrayList的结构发生了修改,迭代器会抛出ConcurrentModificationException异常。这种设计可以确保在多线程环境下,当一个线程修改ArrayList时,其他线程在迭代过程中可以立即发现这种修改,从而避免潜在的数据不一致问题。
再可以看下源码中modCount的注释,大意是:
modCount表示ArrayList自从创建以来结构上发生的修改次数。结构修改是指改变列表大小的修改,或者以其他方式扰乱列表,使正在进行的迭代可能产生不正确的结果。
modCount字段用于iterator和listIterator方法返回的迭代器(或列表迭代器)。如果这个字段的值在迭代过程中发生意外的变化,迭代器(或列表迭代器)将在next、remove、previous、set或add操作时抛出ConcurrentModificationException异常。这提供了fail-fast(快速失败)行为,而不是在迭代过程中遇到并发修改时具有不确定性。
子类可以选择使用这个字段。如果子类希望提供fail-fast迭代器(和列表迭代器),那么它只需在其add(int, E)和remove(int)方法(以及覆盖的任何其他导致列表结构修改的方法)中递增此字段。单次调用add(int, E)或remove(int)应该在此字段上增加不超过1次,否则迭代器(和列表迭代器)将抛出虚假的ConcurrentModificationException。如果实现不希望提供fail-fast迭代器,可以忽略此字段。
2.2 分析2-线程安全问题
有个有趣的现象是,这个异常日志仅存在MQ应用中,这是为什么呢?
这其实是一个多线程问题。我们知道,static对象是在类加载时创建的全局对象,它们的生命周期与类的生命周期相同。static对象在程序启动时创建,在程序结束时销毁。这意味着static对象在多个线程之间共享的,可能存在线程安全问题。
翻回去仔细看下代码,可以看到MyMqConsumer定义的stateList是static类型的,是否是否存在线程安全问题呢?

在流量较低的情况下,多个消息不在同一时刻到达,每个线程处理消息将不会争夺static对象,所以不会有问题;
当流量较大情况下,有多个消息可能在同一时刻到达,每个线程处理过程中都会对stateList进行赋值,调用远程RPC接口,它们之间将会争夺static对象,可能存在问题。例如上图中右半部分,线程1还没有处理完消息1时,线程2就开始争抢,那么就可能使ArrayList中modCount != expectedModCount条件满足,从而抛出异常。
三、改进思考
3.1 本问题的优化
经过上述分析,已经清楚问题的产生原因了。对于本问题的优化,其实也比较简单。有如下两种方式可供选择:
1. 在MyMqConsumer调用RPC查询的入参,使用new List来替代原来的类中定义好的static对象;
2. 修改KeyUtil代码,浅拷贝传入的itemList,再进行排序
3.2 类似问题的发现和改进
本问题已经修复,那类似的问题是否可以避免或者减少,将是接下来值得思考的一个问题。为了减少这类问题发生,我结合平时工作过程中的几个阶段,认为可以从以下几个方面进行改进:
- 开发
开发过程中,开发人员需要提升认知和水平,注意代码中可能存在的线程问题;注意编写单元测试,可以通过模拟多线程环境来检测潜在的问题。
- 代码评审
开发完成的代码一定需要进行代码评审,评审过程中架构师需要发挥自己丰富的开发经验和较强的代码直觉,“火眼金睛”,发现代码中的漏洞;当然这对评审人员的要求很高,因为仅通过改动的几行代码发现问题确实是一件很有挑战的事情。如果要有一些自动化工具或者插件,则可以起到事半功倍的效果。这里其实我还没有调研相关的工具,如果有大佬有相关经验欢迎评论交流。
- 测试
测试阶段除了验证正常的业务功能,还需要进行集成测试和性能测试。在集成测试中,将多个模块组合在一起,测试整个系统在多线程环境中的行为,有助于发现模块之间的交互问题。除了继承测试,有时还需要性能测试,性能测试可以发现潜在的竞争条件、死锁、资源争用等多线程问题。
四、小结
最后,我简单总结一下本文内容。本文主要记录和分析日志中的ConcurrentModificationException关键字报警,首先介绍了系统整体架构和关键代码;然后从ArrayList源码和线程安全两个方面分析问题产生原因,最后我提出了修复该问题的方案和类似问题的思考,希望对大家有帮助。

ConcurrentModificationException日志关键字报警引发的思考的更多相关文章
- 曲演杂坛--一条DELETE引发的思考
原文:曲演杂坛--一条DELETE引发的思考 场景介绍: 我们有一张表,专门用来生成自增ID供业务使用,表结构如下: CREATE TABLE TB001 ( ID ,) PRIMARY KEY, D ...
- 由SecureCRT引发的思考和学习
由SecureCRT引发的思考和学习 http://mp.weixin.qq.com/s?__biz=MzAxOTAzMDEwMA==&mid=2652500597&idx=1& ...
- class_copyIvarList方法获取实例变量问题引发的思考
在runtime.h中,你可以通过其中的一个方法来获取实例变量,那就是class_copyIvarList方法,具体的实现如下: - (NSArray *)ivarArray:(Class)cls { ...
- 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考
2018年12月12日18:44:53 一个ScheduledExecutorService启动的Java线程无故挂掉引发的思考 案件现场 不久前,在开发改造公司一个端到端监控日志系统的时候,出现了一 ...
- SQLAlchemy并发写入引发的思考
背景 近期公司项目中加了一个积分机制,用户登录签到会获取登录积分,但会出现一种现象就是用户登录时会增加双倍积分,然后生成两个积分记录.此为问题 问题分析 项目采用微服务架构,下图为积分机制流程 ...
- int.TryParse非预期执行引发的思考 ASP.NET -- WebForm -- 给图片添加水印标记 Windows -- 使用批处理文件.bat删除旧文件
int.TryParse非预期执行引发的思考 问题出现 这天在写一个页面,想谨慎些就用了int.TryParse,结果出问题了. 代码如下: Copy int id = 1000; //Reque ...
- 测试杂谈——一条SQL引发的思考(二)
在前段时间,曾写过一篇关于SQL问题的文章,测试杂谈--一条SQL引发的思考(一). 今天这篇,算是个问题记录吧,问题并不复杂,但对于测试同学而言,确实是个需要关注的点. 问题分析 最近在日常工作中, ...
- Spring之LoadTimeWeaver——一个需求引发的思考---转
原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver——一个需求引 ...
- 解决一道leetcode算法题的曲折过程及引发的思考
写在前面 本题实际解题过程是 从 40秒 --> 24秒 -->1.5秒 --> 715ms --> 320ms --> 48ms --> 36ms --> ...
- 【思考】由安装zabbix至排障php一系列引发的思考
[思考]由安装zabbix至排障php一系列引发的思考 linux的知识点林立众多,很有可能你在排查一个故障的时候就得用到另一门技术的知识: 由于linux本身的应用依赖的库和其它环境环环相扣,但又没 ...
随机推荐
- Codespaces个性化后台服务器配置指南
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 前文概览 在前文<浏览器上写代码,4核8G微软服 ...
- C语言下载minGW地址
https://sourceforge.net/projects/mingw-w64/files/ 下载红框内即可
- Mybatis插件功能
1 插件的作用 在Mybatis执行SQL的生命周期中,会使用插件进行埋点,主要包括Executor.StatementHandler.ParameterHandler和ResultSetHandle ...
- 如何利用电商API接口来获取商品数据
要利用电商API接口来获取商品数据,我们可以按照以下步骤实现: 确定电商平台和API接口 不同的电商平台提供不同的API接口,因此我们需要确定我们要获取商品数据的电商平台,并选择相应的API接口进行调 ...
- Ionic 整合 pixi.js
最近做了个app,上线google play不大顺利,说是有假冒行为,然后改了下icon和名字以及描述,但是没啥信息去上,于是暂时放下搞点别的. 因为近期看到个比较有趣的绘图创意, 于是想通过ioni ...
- 新零售SaaS架构:面向中小连锁的SaaS系统整体规划
零售企业的发展路径 零售企业的发展路径一般可分为以下几个阶段: 单店经营阶段:企业在一个地区或城市开设单个门店.这时,企业需要把精力放在了解当地市场和顾客需求上,这是积累经验和品牌知名度的重要环节.为 ...
- Python基础——数字类型int与float、字符串、列表、元组、字典、集合、可变类型与不可变类型、数据类型总结
文章目录 一 引子 二 数字类型int与float 2.1 定义 2.2 类型转换 2.3 使用 三 字符串 3.1 定义: 3.2 类型转换 3.3 使用 3.3.1 优先掌握的操作 3.3.2 需 ...
- Redis系列之——高级用法
文章目录 一 慢查询 1.1 生命周期 1.2 两个配置 1.2.1 slowlog-max-len 1.2.2 slowlog-max-len 1.2.3 配置方法 1.3 三个命令 1.4 经验 ...
- 单元测验3:亲密关系mooc
单元测验3:亲密关系 查看帮助 返回 1 单选(2分) 在亲密关系中,有关权力的表述,以下说法不太准确的的是? A. 对关系付出越多,权力越大. B. 大部分人会倾向认为,在恋爱关系中,男女应该拥 ...
- Python join拼接
import os print(os.path.join("I","love","you.")) # /XXX 代表的是绝对路径 这个变量之 ...