Size of map written was 1, but number of entries written was 0. 异常分析
背景
一个 Kafka 连接测试类,发送数据时报了上面的异常:
java.util.ConcurrentModificationException: Size of map written was 1, but number of entries written was 0. at org.apache.avro.generic.GenericDatumWriter.writeMap(GenericDatumWriter.java:178)
异常原因分析
java.util.ConcurrentModificationException
异常通常在遍历集合(如 HashMap、ArrayList 等)时,直接修改集合的结构(例如添加或删除元素)而引发。
具体到问题描述中的错误信息:“Size of map written was 1, but number of entries written was 0
”,这表明在遍历 HashMap 时发生了并发修改异常。
- 遍历过程中修改集合:在使用增强型 for 循环或 Iterator 遍历 HashMap 时,如果直接调用 map.remove(key) 方法来删除元素,会导致 ConcurrentModificationException 异常。这是因为 Iterator 在遍历时会检查集合的 modCount(修改次数),如果在遍历过程中集合被修改,modCount 会发生变化,从而触发异常。
- 单线程环境下的并发修改:即使在单线程环境中,如果在遍历集合时直接修改集合的结构,也会抛出 ConcurrentModificationException 异常。这是因为 Iterator 在遍历时依赖于集合的初始状态,任何结构上的修改都会导致异常。
解决方案
使用 Iterator 的 remove 方法:在遍历 HashMap 时,应该使用 Iterator 提供的 remove 方法来删除元素,而不是直接调用 map.remove(key)。这样可以确保 Iterator 能够正确地更新 expectedModCount,从而避免 ConcurrentModificationException 异常。
诡异问题
这个问题发生的很诡异,Map 对象是一个测试 Kafka 转发的静态成员变量,然后在一个 init
方法中存入了一个固定的值,代码如下:
private static Map<String, String> data = new HashMap<>(1);public void init() {data.put("DATA", "test");
}
诡异的是,整个类中就只有这个方法被调用一次的 put 后就没有写该 Map 对象的地方了,理论上不应该出现 Map 的 size 读取和 iterator 迭代器遍历后的总数不一致的情况的。
即使是碰巧两个类同时执行了 put 操作,但是 HashMap 类的 put 操作同一个 key 的时候应该不涉及到 remove 操作才对。
启示录
类的私有静态成员变量存在并发问题么?答案是肯定的,静态私有成员变量被所有该类的实例对象共享的,所以存在并发问题。
因此编码时应该理清楚类的成员变量的特点,确定所有实例共享的属性可以设计为 static
,同时必须在对该变量的操作函数中做好并发控制。 否则,定义为类成员变量就可以了,可以避免本文这种诡异的问题。
而本例中定义的 Map 成员变量实际是一个全局且初始化一次的变量,可以放在静态代码块中初始化,或者直接定义为非 static 属性。