问题
问题大概是这样:在订单生成时,会根据设置的一个快递策略的优先级进行订单快递的选择。快递优先级例如这样配置:顺丰快递,优先级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。
关注公众号,阅读更多精彩好文