Collections.unmodifiableSet 导致的ConcurrentModificationException问题

235 阅读2分钟

先说结论

Collections.unmodifiableSet()方法封装的Set,并不是产生一个新的Set,只是对原有Set进行的封装。故对其遍历时,如果对原有的Set进行修改,会导致ConcurrentModificationException问题。

背景

线上服务老是偶发性的出现ConcurrentModificationException异常。根据日志堆栈信息定位到DubboCloudRegistry类的220行

相关代码如下

image.png 要出现这个错误,肯定是在遍历的时候对该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);
        }
    });
}

运行后如下:

image.png

所以想到这个for循环里面是不是有对这个Set进行修改的操作。查看这个方法许久,也没有发现相关代码。而且里面调用的循环的时候根本就没有把该Set传进去,根本就修改不了。。。

于是想到有没有其他地方修改该Set,根据堆栈信息,代码继续往上查看。

image.png 查看getServices方法

image.png

image.png

image.png 返回的是一个unmodifiableSet。开始以为unmodifiableSet方法返回的Set 和new HashSet()一样,都是返回的新Set,而且unmodifiableSet是一个不可以修改的Set。然后问题就这样卡住了。。。

仔细查看堆栈

image.png 报错是由于遍历unmodifiableSet引起的,这时候我想到是不是可能是由于unmodifiableSet封装的Set导致的。 查看其封装的Set

image.png 因为该Set是一个全局变量,找到该Set更新的地方,就一个地方

image.png 下面正好有一个地方把原有的Set给清空了。 找到该方法的调用

image.png 正好是监听服务心跳的时候。 可以想到服务进行心跳的时候,清空了Set,正好另一个线程在遍历该Set,导致ConcurrentModificationException异常。

写一个Demo试试:

image.png 运行,果然如此

image.png

解决

因为是第三方源码,只能在本地项目中重写一个相同的类,重写获取Set的方法,改成直接返回一个新的Set。

image.png