<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"Cambria Math"; panose-1:2 4 5 3 5 4 6 3 2 4; mso-font-charset:0; mso-generic-font-family:roman; mso-font-pitch:variable; mso-font-signature:-1610611985 1107304683 0 0 159 0;} @font-face {font-family:Calibri; panose-1:2 15 5 2 2 2 4 3 2 4; mso-font-charset:0; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:-1610611985 1073750139 0 0 159 0;} @font-face {font-family:"Comic Sans MS"; panose-1:3 15 7 2 3 3 2 2 2 4; mso-font-charset:0; mso-generic-font-family:script; mso-font-pitch:variable; mso-font-signature:647 0 0 0 159 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-unhide:no; mso-style-qformat:yes; mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:11.0pt; font-family:"Calibri","sans-serif"; mso-fareast-font-family:宋体; mso-bidi-font-family:"Times New Roman"; mso-font-kerning:1.0pt;} .MsoChpDefault {mso-style-type:export-only; mso-default-props:yes; font-size:10.0pt; mso-ansi-font-size:10.0pt; mso-bidi-font-size:10.0pt; mso-ascii-font-family:Calibri; mso-fareast-font-family:宋体; mso-hansi-font-family:Calibri; mso-font-kerning:0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:841.9pt 595.3pt; mso-page-orientation:landscape; margin:1.0cm 1.0cm 1.0cm 1.0cm; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->
并发修改异常是一个在系统中很常见的异常,但是很多开发人员对这个异常有误解,一定认为是在并发修改时修改产生该异常,什么是并发修改了,就是在多线程中修改,但是在检查自己带码时候并没有使用到多线程,所以就一直找不到问题根源,根源请参见下面。。。。
ConcurrentModificationException 的解释
我们可以查看 JDKAPI 来参考该异常的解释 : 当方法检测到对象的并发修改时,抛出此异常。
我门系统中大部分在是在使用 Collection 上进行迭代时抛出异常,但是我们系统都是使用的单线程,为什么会发生该异常了
我们参考下面错误代码,一个简单的需求,用户获取一个集合数据,需要讲不满足用户需求的数据删除掉
代码:
// 从业务方法中查询数据
List<String> list = service .queryResult();
for (String value : list)
{
//bbb 数据不是用户需要的数据,我们需要移除
if (value.equals( "aaa" ))
{
// 将不满足用户数据删除
list.remove(value);
}
}
运行结果 :
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.AbstractList$Itr.checkForComodification( AbstractList.java:372 )
at java.util.AbstractList$Itr.next( AbstractList.java:343 )
at com.demo.ConcurrentExceptionDemo.main( ConcurrentExceptionDemo.java:24 )
我们可以发现上面的代码并没有什么并发问题,到底是什么原因发生该异常,我们可以再次查看 API 文档
引用文档上面的话:“ 此异常不会始终指出对象已经由不同 线程并发修改。如果单线程发出违反对象协定的方法调用序列,则该对象可能抛出此异常”现在我们知道了单线程也是会抛出该异常的,由于我们违反了对象的协定的方法调用序列,但是我们是在哪里违反了协定了?我们再次查看 API 文档上面发现 : “行该操作的迭代器称为快速失败 迭代器,因为迭代器很快就完全失败,而不会冒着在将来某个时间任意发生不确定行为的风险”,这句话什么意思了?看了很久没有搞明白,所以直接查看源代码发现:、
final void checkForComodification () {
if ( modCount != expectedModCount )
throw new ConcurrentModificationException();
}
}
我们使用的迭代器在调用 next ()调用了改方法 checkForComodification () ,当每次检查 modCount != expectedModCount 就会抛出该异常,
下面我们来再次分析我们的代码
modCount 是集合每次在调用 remove 和 add 方法时都会记录修改次数 modCount++ , 但是我们迭代产生后 expectedModCount 就是一个固定值了,所以你只要在迭代时修改了集合就会造成 modCount != expectedModCount ,所以就抛出了 ConcurrentModificationException
现在我们应该已经搞清楚了改异常产生的原因了,但是我们还是要实现上面的需求,不满足要求还是要删除,我们代码该怎么实现了
方案一 : 通过调用迭代器的 remove 方法来实现删除
Iterator<String> ite = list.iterator();
while (ite.hasNext())
{
//aaa 数据不是用户需要的数据,我们需要移除
if (ite.next().equals( "aaa" ))
{
list.remove( "aaa" );
}
}
我们来看看为什么调用迭代器的 remove 方法不会发生该异常了,下面是该迭代器的 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();
}
}
我们发现上面加粗标红的字体就应该明白原因了吧。
方案二 : 首先获取需要删除的元素,然后通过迭代要删除的元素来删除
List<String> delResults = new ArrayList<String>();
for (String value : list)
{
//bbb 数据不是用户需要的数据,我们需要移除
if (value.equals( "bbb" ))
{
delResults.add(value);
}
}
// 然后迭代要删除的集合来删除
for (String delValue : delResults)
{
list.remove(delValue);
}
这样也是没有问题的,但是我们发现方案二好像比方法一多做了线性循环。
集合抛出 ConcurrentModificationException 进阶
我们下面来看一下下面代码
package com.demo;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.SynchronousQueue;
public class HiddenIterator
{
private Set<Integer> set = new HashSet<Integer>();
public void add(Integer i)
{
set.add(i);
}
public void remove(Integer i)
{
set.remove(i);
}
public void addTenThings()
{
Random rand = new Random();
for (int i =0;i<10;i++)
{
add(rand.nextInt());
System.out.println("DEBUG add ten elements to set : " + set);
}
}
public static void main(String []args)
{
final HiddenIterator hi = new HiddenIterator();
new Thread(){
public void run() {
hi.addTenThings();
};
}.start();
hi.addTenThings();
}
}
我们来看看改代码,暂时不要管上面标红的行 ,就是在添加了一个元素打印集合中的值 ,我们看看程序的原意就是通过两个线程来向集合中添加元素,运行该代码发现下面结果
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry( HashMap.java:793 )
at java.util.HashMap$KeyIterator.next( HashMap.java:828 )
at java.util.AbstractCollection.toString( AbstractCollection.java:421 )
at java.lang.String.valueOf( String.java:2826 )
at java.lang.StringBuilder.append( StringBuilder.java:115 )
at com.demo.HiddenIterator.addTenThings( HiddenIterator.java:28 )
at com.demo.HiddenIterator$1.run( HiddenIterator.java:37 )
我们上面已经知道集合是在做迭代集合的时候才会发生该异常,我们可以看集合的 add() 方法里也完全没有抛出该异常的代码。这就百思不得其解了,然后一句一句的代码查看,原来发现问题原因处在了我们上面标红的代码行。我们其实只要注释掉打印语句就不会发生该异常了
原来我们打印集合时,会自动调用 toString ()方法,我们现在查看 toString ()方法源码
public String toString () {
Iterator<E> i = iterator();
if (! i.hasNext())
return "[]" ;
StringBuilder sb = new StringBuilder();
sb.append( '[' );
for (;;) {
E e = i.next();
sb.append(e == this ? "(this Collection)" : e);
if (! i.hasNext())
return sb.append( ']' ).toString();
sb.append( ", " );
}
原来在调用集合 toString 方法时做了集合的迭代操作,所以导致了改异常的产生,因此我们在使用集合时要注意集合的一些隐式迭代操作,整理了一下集合的 hashCode,equals,toString,containsAll,removeAll,retainAll 方法都做了隐式迭代,所以我们在并发环境下将集合作为另一个容器的元素和一个容易的 key 时要注意,以及把容器作为参数的构造函数时都会对容器进行迭代,因此在这些操作下都可能会引起 ConcurrentModificationException