先说结论
Collections.unmodifiableSet()方法封装的Set,并不是产生一个新的Set,只是对原有Set进行的封装。故对其遍历时,如果对原有的Set进行修改,会导致ConcurrentModificationException问题。
背景
线上服务老是偶发性的出现ConcurrentModificationException异常。根据日志堆栈信息定位到DubboCloudRegistry类的220行
相关代码如下
要出现这个错误,肯定是在遍历的时候对该Set进行了修改操作。
demo如下:
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("1");
set.add("2");
set.add("3");
set.forEach(x->{
System.out.println(x);
if ("2".equals(x)){
set.remove(x);
}
});
}
运行后如下:
所以想到这个for循环里面是不是有对这个Set进行修改的操作。查看这个方法许久,也没有发现相关代码。而且里面调用的循环的时候根本就没有把该Set传进去,根本就修改不了。。。
于是想到有没有其他地方修改该Set,根据堆栈信息,代码继续往上查看。
查看getServices方法
返回的是一个unmodifiableSet。开始以为unmodifiableSet方法返回的Set
和new HashSet()一样,都是返回的新Set,而且unmodifiableSet是一个不可以修改的Set。然后问题就这样卡住了。。。
仔细查看堆栈
报错是由于遍历unmodifiableSet引起的,这时候我想到是不是可能是由于unmodifiableSet封装的Set导致的。
查看其封装的Set
因为该Set是一个全局变量,找到该Set更新的地方,就一个地方
下面正好有一个地方把原有的Set给清空了。
找到该方法的调用
正好是监听服务心跳的时候。
可以想到服务进行心跳的时候,清空了Set,正好另一个线程在遍历该Set,导致ConcurrentModificationException异常。
写一个Demo试试:
运行,果然如此
解决
因为是第三方源码,只能在本地项目中重写一个相同的类,重写获取Set的方法,改成直接返回一个新的Set。