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