在使用Iterator遍历容器类的过程中,如果对容器的内容进行增加和删除,就会出现ConcurrentModificationException异常。该异常的分析和解决方案详见博文《Java ConcurrentModificationException 异常分析与解决方案》和《解决ArrayList的ConcurrentModificationException》。本文展示一种隐蔽性较高的ConcurrentModificationException异常场景,并给出解决方案。
代码示例如下:
1 public class ThreadTest { 2 private static Map<Pattern, Integer> ITEM_MAP = null; //private static ConcurrentMap<Pattern, Integer> ITEM_MAP = null; 3 private static int getPortLevel(String portName) { 4 int level = 0; 5 if(ITEM_MAP == null) { 6 ITEM_MAP = new HashMap<Pattern, Integer>(); //ITEM_MAP = new ConcurrentHashMap<Pattern, Integer>(); 7 ITEM_MAP.put(Pattern.compile("^Cpos+|^Pos+"), 1); 8 System.out.println(""+Thread.currentThread().getName()+": cur="+ITEM_MAP.size()); 9 ITEM_MAP.put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2); 10 ITEM_MAP.put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3); 11 ITEM_MAP.put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4); 12 ITEM_MAP.put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5); 13 ITEM_MAP.put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6); 14 ITEM_MAP.put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7); 15 ITEM_MAP.put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8); 16 System.out.println(""+Thread.currentThread().getName()+": Map="+ITEM_MAP); //此句可能抛出ConcurrentModificationException异常 17 } 18 19 //ph1:1353986467,ph2:1100489929,equals=false 20 //System.out.println("ph1:"+Pattern.compile("^Cpos+|^Pos+").hashCode()+",ph2:"+Pattern.compile("^Cpos+|^Pos+").hashCode()+ 21 // ",equals="+(Pattern.compile("^Cpos+|^Pos+").equals(Pattern.compile("^Cpos+|^Pos+")))); 22 23 Iterator<Entry<Pattern, Integer>> iter = ITEM_MAP.entrySet().iterator(); 24 System.out.println(""+Thread.currentThread().getName()+": Map size="+ITEM_MAP.size()); 25 while(iter.hasNext()) { 26 Entry<Pattern, Integer> entry = iter.next(); //此句可能抛出ConcurrentModificationException异常 27 if(entry.getKey().matcher(portName).find()) { 28 level = entry.getValue(); 29 break; 30 } 31 } 32 return level; 33 } 34 35 public static void main(String[] args) { 36 Thread thread1 = new Thread(){ 37 @Override 38 public void run() { 39 System.out.println(""+Thread.currentThread().getName()+", Value="+getPortLevel("400GE1/2/3")); 40 }; 41 }; 42 Thread thread2 = new Thread(){ 43 @Override 44 public void run() { 45 System.out.println(""+Thread.currentThread().getName()+", Value="+getPortLevel("400GE1/2/3")); 46 }; 47 }; 48 thread1.start(); 49 thread2.start(); 50 } 51 }
可见,getPortLevel()内ITEM_MAP的初始化类似懒汉式单例,因此存在多线程问题。在多线程环境下,上述代码运行结果多种多样,例如:
1) 线程0调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程1紧随其后调用getPortLevel(),并跳过初始化分支(map!=null),开始遍历;此时ITEM_MAP内刚插入一个键值对,线程1遍历匹配不到,返回Value=0。
线程0初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
Thread-0: cur=1 Thread-1: Map size=1 Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^Cpos+|^Pos+=1, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2} Thread-0: Map size=8 Thread-1, Value=0 Thread-0, Value=8
2) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
Pattern.compile()会new一个Pattern对象并返回,而相同模式字符串编译后的Pattern对象hashcode不同且equals返回false,因此会插入"重复"的key(Map size=15)。
线程0和线程1初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
Thread-0: cur=1 Thread-1: cur=1 Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7} Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7} Thread-1: Map size=15 Thread-0: Map size=15 Thread-0, Value=8 Thread-1, Value=8
注意,HashMap会根据key对象的hashcode和equals方法判断key的重复性。因此,使用HashMap时一般要覆写key对象的hashcode和equals方法并确保其正确性,以免插入“重复”的key。
3) 线程0调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程1紧随其后调用getPortLevel(),并跳过初始化分支(map!=null),开始遍历;此时线程0正在向ITEM_MAP内插入键值对,因遍历期间条目数改变而触发ConcurrentModificationException。
线程0初始化ITEM_MAP完毕,开始遍历并匹配成功,返回Value=8。
Thread-0: cur=1
Thread-1: Map size=1
Exception in thread "Thread-1" Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^Cpos+|^Pos+=1, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2}
Thread-0: Map size=8
java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(Unknown Source)
at java.util.HashMap$EntryIterator.next(Unknown Source)
at java.util.HashMap$EntryIterator.next(Unknown Source)
Thread-0, Value=8
4) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程0初始化ITEM_MAP完毕(Map size=11),开始遍历;此时线程1正在向ITEM_MAP内插入键值对,因此线程0触发ConcurrentModificationException。
线程1初始化ITEM_MAP完毕(Map size=15),开始遍历并匹配成功,返回Value=8。
Thread-0: cur=1 Thread-1: cur=1 Thread-0: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^Cpos+|^Pos+=1} Thread-0: Map size=11 Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7} Thread-1: Map size=15 Thread-1, Value=8 Exception in thread "Thread-0" java.util.ConcurrentModificationException
注意,两个线程先后执行getPortLevel()内ITEM_MAP = new HashMap<Pattern, Integer>()语句时,后者new出的对象会覆盖前者。此时很可能丢失前者已插入的键值对,导致两个线程打印的MAP条目数不同。
5) 线程0和线程1同时调用getPortLevel()进入if(ITEM_MAP == null)分支,初始化ITEM_MAP。
线程0初始化ITEM_MAP完毕,打印ITEM_MAP内容(内含遍历操作);此时线程1正在向ITEM_MAP内插入键值对,因此线程0触发ConcurrentModificationException。
线程1初始化ITEM_MAP完毕(Map size=15),开始遍历并匹配成功,返回Value=8。
Thread-0: cur=1 Thread-1: cur=1 Thread-1: Map={^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$=6, ^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$=7, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^Cpos+|^Pos+=1, ^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$=3, ^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$=4, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$=8, ^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$=5, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2, ^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$=2} Thread-1: Map size=15 Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextNode(Unknown Source) at java.util.HashMap$EntryIterator.next(Unknown Source) at java.util.HashMap$EntryIterator.next(Unknown Source) at java.util.AbstractMap.toString(Unknown Source) at java.lang.String.valueOf(Unknown Source) at java.lang.StringBuilder.append(Unknown Source) at ThreadTest.getPortLevel(ThreadTest.java:16) Thread-1, Value=8
以下提供两种修改方案:
1. ConcurrentHashMap
修改点如原代码第2行和第6行注释所示,非常简单。该方案仍存在插入"重复"key的问题,但这并非ConcurrentHashMap本身的缺陷。
2. 将ITEM_MAP的初始化放在static语句块内:
1 private static final Map<Pattern, Integer> ITEM_MAP = new HashMap<Pattern, Integer>(); 2 static { 3 ITEM_MAP.put(Pattern.compile("^Cpos+|^Pos+"), 1); 4 ITEM_MAP.put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2); 5 ITEM_MAP.put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3); 6 ITEM_MAP.put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4); 7 ITEM_MAP.put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5); 8 ITEM_MAP.put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6); 9 ITEM_MAP.put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7); 10 ITEM_MAP.put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8); 11 } 12 private static int getPortLevel(String portName) { 13 int level = 0; 14 15 Iterator<Entry<Pattern, Integer>> iter = ITEM_MAP.entrySet().iterator(); 16 while(iter.hasNext()) { 17 Entry<Pattern, Integer> entry = iter.next(); 18 if(entry.getKey().matcher(portName).find()) { 19 level = entry.getValue(); 20 break; 21 } 22 } 23 return level; 24 }
或者直接在声明时快速初始化:
1 private static final Map<Pattern, Integer> ITEM_MAP = new LinkedHashMap<Pattern, Integer>() { 2 { //可将常见类型的端口放在MAP前面,遍历时利用LinkedHashMap的有序性提高遍历速度 3 put(Pattern.compile("^Cpos+|^Pos+"), 1); 4 put(Pattern.compile("^(GE)([0-9/]+)$|^(GE)([0-9/]+):([0-9/]+)$"), 2); 5 put(Pattern.compile("^(10GE)([0-9/]+)$|^(10GE)([0-9/]+):([0-9/]+)$"), 3); 6 put(Pattern.compile("^(40GE)([0-9/]+)$|^(40GE)([0-9/]+):([0-9/]+)$"), 4); 7 put(Pattern.compile("^(50GE)([0-9/]+)$|^(50GE)([0-9/]+):([0-9/]+)$"), 5); 8 put(Pattern.compile("^(100GE)([0-9/]+)$|^(100GE)([0-9/]+):([0-9/]+)$"), 6); 9 put(Pattern.compile("^(200GE)([0-9/]+)$|^(200GE)([0-9/]+):([0-9/]+)$"), 7); 10 put(Pattern.compile("^(400GE)([0-9/]+)$|^(400GE)([0-9/]+):([0-9/]+)$"), 8); 11 } 12 };
注意,采用该方案时,应确保其他地方不会对ITEM_MAP进行增、删操作(仅靠final修饰无法保证这点)。若出现该情况,通常意味着深层次的设计缺陷,而试图在编码层面修复往往适得其反。例如,作者遇到的一种错误的写法示例如下(实际代码很复杂):
1 private LinkedHashSet<String> nameSet; 2 public LinkedHashSet<String> getNames() { 3 System.out.println(""+Thread.currentThread().getName()+", nameSet="+nameSet); 4 5 if(CollectionUtils.isEmpty(nameSet)) { 6 nameSet = new LinkedHashSet<String>(); 7 nameSet.add("Jack"); 8 nameSet.add("Jame"); 9 } 10 11 //return nameSet; 12 //无论是new时以nameSet初始化还是new出对象后调用addAll(nameSet),均可能因为容器内部迭代而触发ConcurrentModificationException。 13 //但本例可保证外部不会直接修改nameSet,所以此处复制对象是安全的。 14 LinkedHashSet<String> names = new LinkedHashSet<String>(nameSet); 15 System.out.println(""+Thread.currentThread().getName()+", names="+names); 16 return names; 17 } 18 public void addNames() { 19 getNames().add("Lucy"); 20 getNames().add("Beth"); 21 } 22 public void showNames() { 23 for(String name : getNames()) { 24 System.out.println("This is "+name); 25 } 26 }
当线程0调用showNames()的同时,线程1在调用addNames(),就可能导致ConcurrentModificationException异常。当然,本例过于简单,很难真正地触发异常,仅作示例而已。
注意getNames()内复制nameSet对象的写法。该写法试图修复ConcurrentModificationException异常,但因为每次调用都会重新new对象,实际上addNames()无法将Lucy和Beth添加到名字表里!可见,这种试图修复少见异常的尝试反而导致严重的逻辑错误。