java.util.ConcurrentModificationException异常问题详解

更新时间:2023-06-15 18:04:12 阅读: 评论:0

java.util.ConcurrentModificationException异常问题详解
环境:JDK 1.8.0_111
在Java开发过程中,使⽤iterator遍历集合的同时对集合进⾏修改就会出现java.util.ConcurrentModificationException异常,本⽂就以ArrayList为例去理解和解决这种异常。
⼀、单线程情况下问题分析及解决⽅案
1.1 问题复现
先上⼀段抛异常的代码。
1public void test1()  {
2        ArrayList<Integer> arrayList = new ArrayList<>();
3for (int i = 0; i < 20; i++) {
4            arrayList.add(Integer.valueOf(i));
5        }
6
7// 复现⽅法⼀
8        Iterator<Integer> iterator = arrayList.iterator();
9while (iterator.hasNext()) {
10            Integer integer = ();
11if (integer.intValue() == 5) {
12                ve(integer);
13            }
14        }
15
16// 复现⽅法⼆
17        iterator = arrayList.iterator();
18for (Integer value : arrayList) {
19            Integer integer = ();
20if (integer.intValue() == 5) {
21                ve(integer);
22            }
23        }
24    }
在这个代码中展⽰了两种能抛异常的实现⽅式。
1.2、问题原因分析
先来看实现⽅法⼀,⽅法⼀中使⽤Iterator遍历ArrayList,抛出异常的是()。看下Iterator next⽅法实现源码
1public E next() {
2            checkForComodification();
3int i = cursor;
4if (i >= size)
5throw new NoSuchElementException();
6            Object[] elementData = ArrayList.this.elementData;
7if (i >= elementData.length)
8throw new ConcurrentModificationException();
9            cursor = i + 1;
10return (E) elementData[lastRet = i];
11        }
12
13final void checkForComodification() {
14if (modCount != expectedModCount)
15throw new ConcurrentModificationException();
16        }
在next⽅法中⾸先调⽤了checkForComodification⽅法,该⽅法会判断modCount是否等于expectedModCount,不等于就会抛出
java.util.ConcurrentModificationExcepiton异常。
我们接下来跟踪看⼀下modCount和expectedModCount的赋值和修改。
modCount是ArrayList的⼀个属性,继承⾃抽象类AbstractList,⽤于表⽰ArrayList对象被修改次数。
1protected transient int modCount = 0;
整个ArrayList中修改modCount的⽅法⽐较多,有add、remove、clear、ensureCapacityInternal等,凡是设计到ArrayList对象修改的都会⾃增modCount属性。
在创建Iterator的时候会将modCount赋值给expectedModCount,在遍历ArrayList过程中,没有其他地⽅可以设置expectedModCount了,因此遍历过程中expectedModCount会⼀直保持初始值20(调⽤add⽅法添加了20个元素,修改了20次)。
1int expectedModCount = modCount; // 创建对象时初始化
遍历的时候是不会触发modCount⾃增的,但是遍历到integer.intValue() == 5的时候,执⾏了⼀次ve(integer),这⾏代码执⾏后modCount++变为了21,但此时的expectedModCount仍然为20。
1final void checkForComodification() {
2if (modCount != expectedModCount)
3throw new ConcurrentModificationException();
4        }
在执⾏next⽅法时,遇到modCount != expectedModCount⽅法,导致抛出异常java.util.ConcurrentModificationException。
明⽩了抛出异常的过程,但是为什么要这么做呢?很明显这么做是为了阻⽌程序员在不允许修改的时候修改对象,起到保护作⽤,避免出现未知异常。引⽤⽹上的⼀段解释,
Iterator 是⼯作在⼀个独⽴的线程中,并且拥有⼀个 mutex 锁。
Iterator 被创建之后会建⽴⼀个指向原来对象的单链索引表,当原来的对象数量发⽣变化时,这个索引表的内容不会同步改变。
当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
所以 Iterator 在⼯作的时候是不允许被迭代的对象被改变的。但你可以使⽤ Iterator 本⾝的⽅法 remove() 来删除对象, ve() ⽅法会在删除当前迭代对象的同时维护索引的⼀致性。
再来分析下第⼆种for循环抛异常的原因:
1public void forEach(Consumer<? super E> action) {
2        quireNonNull(action);
3final int expectedModCount = modCount;
4        @SuppressWarnings("unchecked")
5final E[] elementData = (E[]) this.elementData;
6final int size = this.size;
7for (int i=0; modCount == expectedModCount && i < size; i++) {
8            action.accept(elementData[i]);
9        }
10if (modCount != expectedModCount) {
11throw new ConcurrentModificationException();
12        }
13    }
在for循环中⼀开始也是对expectedModCount采⽤modCount进⾏赋值。在进⾏for循环时每次都会有判定条件modCount == expectedModCount,当执⾏完ve(integer)之后,该判定条件返回fal退出循环,然后执⾏if语句,结果同样抛出java.util.ConcurrentModificationException异常。
这两种复现⽅法实际上都是同⼀个原因导致的。
1.3 问题解决⽅案
上述的两种复现⽅法都是在单线程运⾏的,先来说明单线程中的解决⽅案:
1public void test2() {
2        ArrayList<Integer> arrayList = new ArrayList<>();
3for (int i = 0; i < 20; i++) {
4            arrayList.add(Integer.valueOf(i));
5        }
6
7        Iterator<Integer> iterator = arrayList.iterator();
8while (iterator.hasNext()) {
9            Integer integer = ();
10if (integer.intValue() == 5) {
11                ve();
12            }
13        }
14    }
这种解决⽅案最核⼼的就是调⽤ve()⽅法。我们看看该⽅法源码为什么这个⽅法能避免抛出异常
1public void remove() {
2if (lastRet < 0)
3throw new IllegalStateException();
4            checkForComodification();
5
6try {
7                ve(lastRet);
8                cursor = lastRet;
9                lastRet = -1;
10                expectedModCount = modCount;
11            } catch (IndexOutOfBoundsException ex) {
12throw new ConcurrentModificationException();
13            }
14        }
在ve()⽅法中,同样调⽤了ArrayList⾃⾝的remove⽅法,但是调⽤完之后并⾮就return了,⽽是expectedModCount = modCount重置了expectedModCount值,使⼆者的值继续保持相等。
针对forEach循环并没有修复⽅案,因此在遍历过程中同时需要修改ArrayList对象,则需要采⽤iterator遍历。
上⾯提出的解决⽅案调⽤的是ve()⽅法,如果不仅仅是想调⽤remove⽅法移除元素,还想增加元素,或者替换元素,是否可以呢?浏览Iterator源码可以发现这是不⾏的,Iterator只提供了remove⽅法。
但是ArrayList实现了ListIterator接⼝,ListIterator类继承了Iter,这些操作都是可以实现的,使⽤⽰例如下:
1public void test3() {
2        ArrayList<Integer> arrayList = new ArrayList<>();
3for (int i = 0; i < 20; i++) {
4            arrayList.add(Integer.valueOf(i));
5        }
6
7        ListIterator<Integer> iterator = arrayList.listIterator();
8while (iterator.hasNext()) {
9            Integer integer = ();
10if (integer.intValue() == 5) {
11                iterator.t(Integer.valueOf(6));
12                ve();
13                iterator.add(integer);
14            }
摩登家庭 美剧15        }
16    }
⼆、多线程情况下的问题分析及解决⽅案
单线程问题解决了,再来看看多线程情况。
2.1 问题复现
1public void test4() {
2        ArrayList<Integer> arrayList = new ArrayList<>();
3for (int i = 0; i < 20; i++) {
4            arrayList.add(Integer.valueOf(i));
5        }
6
7        Thread thread1 = new Thread(new Runnable() {
8            @Override
9public void run() {
sadf
10                ListIterator<Integer> iterator = arrayList.listIterator();
11while (iterator.hasNext()) {
12                    System.out.println("thread1 " + ().intValue());
13try {
合肥mba学校14                        Thread.sleep(1000);
15                    } catch (InterruptedException e) {
16                        e.printStackTrace();
17                    }
18                }
19            }
20        });
21
22        Thread thread2 = new Thread(new Runnable() {
23            @Override
24public void run() {
25                ListIterator<Integer> iterator = arrayList.listIterator();
26while (iterator.hasNext()) {
27                    System.out.println("thread2 " + ().intValue());
28                    ve();
29                }
30            }
31        });
32        thread1.start();
33        thread2.start();
34    }
在个测试代码中,开启两个线程,⼀个线程遍历,另外⼀个线程遍历加修改。程序输出结果如下
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
urgethread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
外贸业务员英文简历thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.(ArrayList.java:851)
at com.snow.ExceptionTest$1.run(ExceptionTest.java:74)
at java.lang.Thread.run(Thread.java:745)
Process finished with exit code 0
2.2 问题分析
从上⾯代码执⾏结果可以看出thread2 遍历结束后,thread1 sleep完1000ms准备遍历第⼆个元素,next的时候抛出异常了。我们从时间点分析⼀下抛异常的原因
时间点dCount pectedModCount pectedModCount
thread start,初始化iterator202020
两个thread都是使⽤的同⼀个arrayList,thread2修改完后modCount = 21,此时thread2的expectedModCount = 21 可以⼀直遍历到结束;thread1的expectedModCount仍然为20,因为thread1的expectedModCount只是在初始化的时候赋值,其后并未被修改过。因此当arrayList的modCount被thread2修改为21之后,thread1想继续遍历必定会抛出异常了。
在这个⽰例代码⾥⾯,两个thread,每个thread都有⾃⼰的iterator,当thread2通过iterator⽅法修改expectedModCount必定不会被thread1感知到。这个跟ArrayList ⾮线程安全是⽆关的,即使这⾥⾯的ArrayList换成Vector也是⼀样的结果,不信上测试代码:
1public void test5() {
2        Vector<Integer> vector = new Vector<>();
3for (int i = 0; i < 20; i++) {
4            vector.add(Integer.valueOf(i));
5        }
debon6
7        Thread thread1 = new Thread(new Runnable() {
8            @Override
9public void run() {
10                ListIterator<Integer> iterator = vector.listIterator();
11while (iterator.hasNext()) {
12                    System.out.println("thread1 " + ().intValue());
13try {
14                        Thread.sleep(1000);
15                    } catch (InterruptedException e) {
16                        e.printStackTrace();
17                    }
18                }
19            }
20        });
21
22        Thread thread2 = new Thread(new Runnable() {
23            @Override
24public void run() {
25                ListIterator<Integer> iterator = vector.listIterator();
26while (iterator.hasNext()) {
27                    Integer integer = ();
28                    System.out.println("thread2 " + integer.intValue());
29if (integer.intValue() == 5) {
30                        ve();
31                    }
32                }
33            }
34        });
35        thread1.start();
36        thread2.start();
37    }
执⾏后输出结果为:
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
英语手抄报内容
thread2 14delicious的反义词
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
at java.util.(Vector.java:1137)
at com.snow.ExceptionTest$3.run(ExceptionTest.java:112)
at java.lang.Thread.run(Thread.java:745)
Process finished with exit code 0
test5()⽅法执⾏结果和test4()是相同的,那如何解决这个问题呢?
2.3 多线程下的解决⽅案
2.3.1 ⽅案⼀:iterator遍历过程加同步锁,锁住整个arrayList  1public static void test5() {
2        ArrayList<Integer> arrayList = new ArrayList<>();
3for (int i = 0; i < 20; i++) {
4            arrayList.add(Integer.valueOf(i));
5        }
6
7        Thread thread1 = new Thread(new Runnable() {
8            @Override
9public void run() {
10synchronized (arrayList) {
11                    ListIterator<Integer> iterator = arrayList.listIterator();
12while (iterator.hasNext()) {
13                        System.out.println("thread1 " + ().intValue()); 14try {
15                            Thread.sleep(100);
16                        } catch (InterruptedException e) {
17                            e.printStackTrace();
18                        }
19                    }
20                }
21            }
22        });
23
24        Thread thread2 = new Thread(new Runnable() {
25            @Override
26public void run() {
27synchronized (arrayList) {
28                    ListIterator<Integer> iterator = arrayList.listIterator();
29while (iterator.hasNext()) {
30                        Integer integer = ();
31                        System.out.println("thread2 " + integer.intValue());
32if (integer.intValue() == 5) {
33                            ve();
34                        }
35                    }
36                }
37            }
38        });
39        thread1.start();
40        thread2.start();
41    }
这种⽅案本质上是将多线程通过加锁来转变为单线程操作,确保同⼀时间内只有⼀个线程去使⽤iterator遍历arrayList,其它线程等待,效率显然是只有单线程的效率。
2.3.2 ⽅案⼆:使⽤CopyOnWriteArrayList,有坑!要明⽩原理再⽤,否则你就呆坑⾥吧。
我们先来看代码,很有意思咯
1public void test6() {
2        List<Integer> list = new CopyOnWriteArrayList<>();
3for (int i = 0; i < 20; i++) {
4            list.add(Integer.valueOf(i));
5        }
6
7        Thread thread1 = new Thread(new Runnable() {
8            @Override
9public void run() {
10                ListIterator<Integer> iterator = list.listIterator();
11while (iterator.hasNext()) {
12                    System.out.println("thread1 " + ().intValue());
13try {
14                        Thread.sleep(1000);
15                    } catch (InterruptedException e) {
16                        e.printStackTrace();
17                    }
18                }
19            }
20        });
21
22        Thread thread2 = new Thread(new Runnable() {
23            @Override
24public void run() {
25for (Integer integer : list) {
26                    System.out.println("thread2 " + integer.intValue());
27if (integer.intValue() == 5) {
28                        ve(integer);
29                    }
30                }
31for (Integer integer : list) {
32                    System.out.println("thread2 again " + integer.intValue());
33                }
34//                ListIterator<Integer> iterator = list.listIterator();
35//                while (iterator.hasNext()) {
36//                    Integer integer = ();
37//                    System.out.println("thread2 " + integer.intValue());
38//                    if (integer.intValue() == 5) {
39//                        ve();
40//                    }
41//                }
42            }
泰州会计培训
43        });
44        thread1.start();
45        thread2.start();
46    }
先不分析,看执⾏结果,这个执⾏结果重点关注字体加粗部分。
thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
清远盛兴中英文学校
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
thread2 again 0
thread2 again 1
thread2 again 2
thread2 again 3
thread2 again 4
thread2 again 6
thread2 again 7

本文发布于:2023-06-15 18:04:12,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/78/961969.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:修改   对象   遍历
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图