专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

记一次线上的ConcurrentModificationException

问题

问题大概是这样:在订单创建时,会根据配置的快递策略优先级进行选快递。快递优先级例如这样配置:顺丰快递,优先级1;中通快递,优先级2;圆通快递,优先级3;汇通快递,优先级4。(优先级的值越小表示优先级越高)。我将整个快递策略优先级放在了缓存里(guava缓存)。然后在选快递的时候从缓存里拿到优先级,为了选快递不出错,先对优先级进行了排序,用的Collections.sort方法,实现了比较器方法,按照快递优先级升序排序(相当于优先级是一个全局变量)。在多线程并发情况下(多个订单同时选快递),出现java.util.ConcurrentModificationException

复现

下面以一个Demo复现问题。

优先级数据结构定义


public class PriorityDto { private Long id; private Long createUserId; private Date createTime; private List<PriorityDetailDto> detailDtos; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getCreateUserId() { return createUserId; } public void setCreateUserId(Long createUserId) { this.createUserId = createUserId; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public List<PriorityDetailDto> getDetailDtos() { return detailDtos; } public void setDetailDtos(List<PriorityDetailDto> detailDtos) { this.detailDtos = detailDtos; } }

public class PriorityDetailDto { private Long detailId; private Long id; private Integer priority; public Long getDetailId() { return detailId; } public void setDetailId(Long detailId) { this.detailId = detailId; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Integer getPriority() { return priority; } public void setPriority(Integer priority) { this.priority = priority; } @Override public String toString() { return "PriorityDetailDto{" + "detailId=" + detailId + ", id=" + id + ", priority=" + priority + '}'; } }

测试代码

public class Demo {

    public static void main(String [] args) throws Exception {

        //快递优先级对于所有线程来说是一个全局变量
        PriorityDto dto = init();

        Runnable runnable = new Runnable() {
            @Override
            public void run(){
                //这里对一个全局变量进行排序
                Collections.sort(dto.getDetailDtos(), new Comparator<PriorityDetailDto>() {
                    @Override
                    public int compare(PriorityDetailDto o1, PriorityDetailDto o2) {
                        return o1.getPriority().compareTo(o2.getPriority());
                    }
                });
                System.out.println(dto.getDetailDtos().toString());
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //使用1000个线程模拟
        for (int i=0; i<1000; i++) {
            executorService.execute(runnable);
        }
    }

    //初始化数据
    private static PriorityDto init() {
        PriorityDto dto = new PriorityDto();
        dto.setId(1L);
        dto.setCreateTime(new Date());
        dto.setCreateUserId(-1L);
        List<PriorityDetailDto> detailDtos = new ArrayList<>();
        PriorityDetailDto detailDto = new PriorityDetailDto();
        detailDto.setDetailId(1L);
        detailDto.setId(1L);
        detailDto.setPriority(2);
        detailDtos.add(detailDto);

        PriorityDetailDto detailDto1 = new PriorityDetailDto();
        detailDto1.setDetailId(2L);
        detailDto1.setId(1L);
        detailDto1.setPriority(3);
        detailDtos.add(detailDto1);

        PriorityDetailDto detailDto2 = new PriorityDetailDto();
        detailDto2.setDetailId(3L);
        detailDto2.setId(1L);
        detailDto2.setPriority(1);
        detailDtos.add(detailDto2);

        dto.setDetailDtos(detailDtos);
        return dto;
    }
}

运行结果

65_1.png

原因

追本溯源看源码,上面使用的是ArrayList的sort方法进行的排序。
在Collections.java中

public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }

在ArrayList.java中

@Override
    @SuppressWarnings("unchecked")
    public void sort(Comparator<? super E> c) {
        final int expectedModCount = modCount;    //1
        Arrays.sort((E[]) elementData, 0, size, c);  //2
        if (modCount != expectedModCount) {      //3
            throw new ConcurrentModificationException(); 
        }   
        modCount++;  //4
    }

1、记下进入方法中的modCount。
2、对数组元素elementData按照比较器c的规则进行排序。
3、判断是否进行了并发修改,如果是就抛异常。
4、modCount自增1。
单线程下看这段代码自然没有问题,但是多线程下就有问题,因为modCount是AbstractList中的一个变量protected transient int modCount = 0;如果多个线程同时对modCount进行并发修改,就会出现modCount != expectedModCount的情况。

解决方法

1、以空间换时间:每个线程进行排序的集合私有化,数据不变,但是排序的集合访问区域只在线程内部。例如:

Runnable runnable = new Runnable() {
            @Override
            public void run(){
                List<PriorityDetailDto> detailDtos = new ArrayList<>(dto.getDetailDtos());

                Collections.sort(detailDtos, new Comparator<PriorityDetailDto>() {
                    @Override
                    public int compare(PriorityDetailDto o1, PriorityDetailDto o2) {
                        return o1.getPriority().compareTo(o2.getPriority());
                    }
                });
                System.out.println(detailDtos.toString());
            }
        };

2、也可以使用lock或synchronized将排序的部分锁起来,或者使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。从性能角度看还是第一种为佳。

总结

遍历一个集合时如何避免ConcurrentModificationException:API文档上也有说的!在迭代时只可以用迭代器进行删除!

单线程情况

(1)使用Iterator提供的remove方法,用于删除当前元素。

(2)建立一个集合,记录需要删除的元素,之后统一删除。

(3)不使用Iterator进行遍历,需要自己保证索引正常。

(4)使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnArrayList,而不是ArrayList。

多线程情况

使用并发集合类,如使用ConcurrentHashMap或者CopyOnWriteArrayList。

关注公众号,阅读更多精彩好文

65_2.png

文章永久链接:https://tech.souyunku.com/45788

未经允许不得转载:搜云库技术团队 » 记一次线上的ConcurrentModificationException

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们