场景复现
对于list,map都有可能出现,这里主要分析list!
@Test
public void testList() {
List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
for (String item : list) {
if (item.equals("1")) {
System.out.println(item);
list.remove(item);
}
}
System.out.println(list.size());
}
@Test
public void testMap() {
HashMap<String, Integer> temp = new HashMap<>();
temp.put("aa", 123);
temp.put("bb", 123);
temp.put("cc", 123);
temp.put("dd", 123);
for (String s : temp.keySet()) {
if (s.equals("aa")) {
temp.put("abcd", 123);
}
}
}
原因分析
基本上所有的集合类都会有一个叫做快速失败的校验机制,当一个集合在被多个线程修改并访问时,就会出现ConcurrentModificationException 校验机制。它的实现原理就是我们经常提到的modCount修改计数器。如果在读列表时,modCount发生变化则会抛出ConcurrentModificationException异常。 这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的。
首先java的foreach循环其实就是根据list对象创建一个Iterator迭代对象,用这个迭代对象来遍历list,相当于list对象中元素的遍历托管给了Iterator,你如果要对list进行增删操作,都必须经过Iterator,否则Iterator遍历时会乱,所以直接对list进行删除时,Iterator会抛出ConcurrentModificationException异常
其实,每次foreach迭代的时候都有两部操作:
- iterator.hasNext() //判断是否有下个元素
- item = iterator.next() //下个元素是什么,并赋值给上面例子中的item变量
hasNext()方法的代码如下:
public E next(){
checkForComodification();
try{
E next=get(cursor);
lastRet=cursor++;
retun next;
}catch(IndexOutOfBoundsException e){
checkForComodification();
throw new NoSuchElementException();
}
}
final void checkForComodification(){
if(modCount!=expectedModCount)
throw new ConcurrentModificationException();
}
}
这时候你会发现这个异常是在next方法的checkForComodification中抛出的,抛出原因是modCount != expectedModCount
- modCount是指这个list对象从new出来到现在被修改次数,当调用List的add或者remove方法的时候,这个modCount都会自动增减;
- expectedModCount是指Iterator现在期望这个list被修改的次数是多少次。
iterator创建的时候modCount被赋值给了expectedModCount,但是调用list的add和remove方法的时候不会同时自动增减expectedModCount,这样就导致两个count不相等,从而抛出异常。
总结:
只有当两个变量值相等时才不会报错。而 list.add()操作和 list.remove(); 操作会使 modCount++; 但是变量 expectedModCount 是内部类的 Itr 子类 的 变量,该子类的实例化方法是:list.iterator(); 实例对象时赋初始值:int expectedModCount = modCount;
所以,不允许在 while(iterator.hasNext()) 循环方法内 有list.add()操作和 list.remove();操作,否则会导致expectedModCount != modCount ,进而导致 throw new ConcurrentModificationException();
解决方法
List
//解决1:使用for i形式不用foreach形式
for (int i = 0; i < list.size(); i++) {
if (list..equals("1")) {
list.remove(i);
}
}
System.out.println(list.toString());
//解决2:迭代器形式,并且remove要采用迭代器的
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
Student next = iterator.next();
if (next.equals("2")) {
//在这个方法中,iterator.remove()方法,但是它多了一个操作:
//expectedModCount = modCount;
iterator.remove();
//list.remove(next);
}
}
System.out.println(list.toString());
//解决3:使用并发容器
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
注意上面第二个解决方式:
iterator.remove();在这个方法中,删除元素实际上调用的就是list.remove()方法,但是它多了一个操作:expectedModCount = modCount;
iterator.remove();
list.remove(next);
iterator.remove()源码如下:
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
多线程情况采用如下:
1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;
2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。
Map
//解决方案:使用并发容器即可:
ConcurrentHashMap<String, Integer> temp = new ConcurrentHashMap<>();
参考文章:
https://www.cnblogs.com/yaohuiqin/p/9355874.html
https://blog.csdn.net/androidboy365/article/details/50540202/