zoukankan      html  css  js  c++  java
  • 一种隐蔽性较高的Java ConcurrentModificationException异常场景

    前言

        在使用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

        6) 改用ConcurrentHashMap(修改点如原代码第2行和第6行注释所示)后,虽然可消除ConcurrentModificationException异常,但仍存在前述插入"重复"key或某线程漏匹配的问题。

            例如:

    Thread-1: Map size=0
    Thread-1, Value=0
    Thread-0: cur=1
    Thread-0: Map={^(100GE)([0-9/]+)|(100GE)([0−9/]+):([0−9/]+)|(100GE)([0−9/]+):([0−9/]+)=6, ^(40GE)([0-9/]+)|(40GE)([0−9/]+):([0−9/]+)|(40GE)([0−9/]+):([0−9/]+)=4, ^(400GE)([0-9/]+)|(400GE)([0−9/]+):([0−9/]+)|(400GE)([0−9/]+):([0−9/]+)=8, ^(200GE)([0-9/]+)|(200GE)([0−9/]+):([0−9/]+)|(200GE)([0−9/]+):([0−9/]+)=7, ^Cpos+|^Pos+=1, ^(GE)([0-9/]+)|(GE)([0−9/]+):([0−9/]+)|(GE)([0−9/]+):([0−9/]+)=2, ^(50GE)([0-9/]+)|(50GE)([0−9/]+):([0−9/]+)|(50GE)([0−9/]+):([0−9/]+)=5, ^(10GE)([0-9/]+)|(10GE)([0−9/]+):([0−9/]+)|(10GE)([0−9/]+):([0−9/]+)=3}
    Thread-0: Map size=8
    Thread-0, Value=8

    修改方案 

        以下提供一种修改方案:

        将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添加到名字表里!可见,这种试图修复少见异常的尝试反而导致严重的逻辑错误。

  • 相关阅读:
    oracle查询锁表解锁语句
    转:js,jQuery 排序的实现,网页标签排序的实现,标签排序
    禁止页面缩放功能
    js 操作 cookie
    random模块
    以及模块的四种形式模块的四种形式和模块的调用
    函数的调用
    函数的返回值
    可变长参数
    函数的重点内容
  • 原文地址:https://www.cnblogs.com/clover-toeic/p/8462370.html
Copyright © 2011-2022 走看看