记一次线上的ConcurrentModificationException

1,135 阅读3分钟

问题

问题大概是这样:在订单创建时,会根据配置的快递策略优先级进行选快递。快递优先级例如这样配置:顺丰快递,优先级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;
    }
}

运行结果

原因

追本溯源看源码,上面使用的是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。

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