尝试重新创建java.util.ConcurrentModificationException

时间:2013-08-16 20:59:48

标签: java multithreading

代码如下所示:这里使用的地图是番石榴地图

private Map<SomeObject, SomeOtherObject> myMap = Maps.newLinkedHashMap();

public Map<SomeObject, SomeOtherObject> getMap() {
  return Maps.newHashMap(myMap);
}

public void putMap(SomeObject a, SomeOtherObject b) {
  myMap.put(a,b);
}

所以,上面提到了java.util.ConcurrentModificationException并试图重新创建场景。但无论我尝试什么,系统似乎都很有弹性。这是我试过的:

 1. Created 'n' threads some of which call getMap() and some call putMap()
 2. Created 'n' threads that only call putMap() and one thread the is in an infinite loop that calls getMap()
 3. Created 2 threads each of which alternates calling getMap() and putMap(). Meaning, thread-1 first calls get then put and thread-2 first calls put and then get.

以上都不起作用,无论是继续运行还是进入OOM。关于如何做到这一点的任何指示?

修改 我相信在返回地图{ConcurrentModificationException}的副本时会抛出Maps.newHashMap(myMap);。在此过程中,迭代器创建一个副本,当迭代器正在工作时,如果地图的内容被修改,则不满意。

6 个答案:

答案 0 :(得分:2)

假设您确实使用com.google.common.collect.Maps,则implementation of newHashMap

public static <K, V> HashMap<K, V> newHashMap(Map<? extends K, ? extends V> map) {
    return new HashMap<K, V>(map);
}

如果我们再查看implementation of HashMap

public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
            DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    putAllForCreate(m);
}

private void  [More ...] putAllForCreate(Map<? extends K, ? extends V> m) {
    for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i 
            = m.entrySet().iterator(); i.hasNext(); ) {
        Map.Entry<? extends K, ? extends V> e = i.next();
        putForCreate(e.getKey(), e.getValue());
    }
}

确实,对newHashMap的调用使用迭代器来遍历地图。正如其他答案已经指出的那样,如果在迭代地图时调用putMap,则会抛出ConcurrentModificationException

你怎么能重现这个?我想说两个线程就足够了:一个重复调用getMap,另一个调用putMap

答案 1 :(得分:1)

使用ConcurrentModificationException迭代CollectionMap时会抛出

Iterator,并且在迭代过程中Collection会被修改(不需要另一个线程,但是)。

如果复制操作使用Iterator,则无法保证在迭代繁忙时其他线程将修改Map,尤其是在Map很小的情况下。

要强制解决问题,您必须强制创建副本的线程等待复制循环中的修改线程。使用Guava库可能会阻止您这样做,但暂时将其替换为手动副本将帮助您查明问题。

以下是使用不同线程强制问题与CountDownLatches同步的基本示例:

public static void main(String[] args) {
    final Map<Integer, Integer> map = new HashMap<>();
    final CountDownLatch readLatch = new CountDownLatch(1);
    final CountDownLatch writeLatch = new CountDownLatch(1);

    for (int i = 0; i < 100; i++) {
        map.put(i, i);
    }

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
                    if (entry.getKey().equals(Integer.valueOf(10))) {
                        try {
                            writeLatch.countDown();
                            readLatch.await();
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();

    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                writeLatch.await();
                map.put(150, 150);
                readLatch.countDown();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }).start();
}

读取线程在某个时刻释放写入线程,然后等待,同时写入线程进行修改,然后允许读取线程恢复循环。

如果您正在调试,插入此类锁存器以强制解决问题可能会有所帮助。请注意,并发问题的后续修复可能会在锁存器到位时死锁。为了解决问题,您可以使用sleep()获得可靠的故障,然后后续修复应该可靠地工作。

答案 2 :(得分:1)

下面的代码示例应该在几毫秒内打印“GOT IT”(即ConcurrentModificationException)。

实质上:代码同时放入并从地图中获取并且不可避免地获得CME。随机部分很重要:如果你继续使用相同的键,你可能无法获得相同的效果。

public class CME {

    private static final Test test = new Test();
    private static final Random rand = new Random();

    public static void main(String[] args) throws InterruptedException {
        Runnable getter = new Runnable() {
            @Override public void run() {
                try {
                    while (!Thread.interrupted()) {
                        Map<String, String> tryit = test.getMap();
                    }
                } catch (ConcurrentModificationException e) {
                    System.out.println("GOT IT!");
                }
            }
        };
        Runnable putter = new Runnable() {
            @Override public void run() {
                while (!Thread.interrupted()) {
                    //use a random component to make sure the map
                    //is actually mutated
                    char c = (char) rand.nextInt();
                    test.putMap(String.valueOf(c), "whatever");
                }
            }
        };
        Thread g = new Thread(getter);
        Thread p = new Thread(putter);
        g.start();
        p.start();
        g.join(); //wait until CME
        p.interrupt(); //then interrupt the other thread
                       //to allow program to exit
    }

    static class Test {
        private Map<String, String> myMap = Maps.newLinkedHashMap();
        public Map<String, String> getMap() { return Maps.newHashMap(myMap); }
        public void putMap(String a, String b) { myMap.put(a, b); }
    }
}

答案 3 :(得分:1)

再现并发错误的技巧通常是在处理过程中产生延迟。

在这种情况下,如果put线程在get地图迭代时添加了一个成员,则抛出异常。

我能够通过以下方式获得良好的结果,通过在hashCode

中添加延迟来人为地减慢迭代(也会进行放置)
import java.util.Map;
import java.util.HashMap;

class CME {
    static class K {
        String value;
        K(String value) { this.value = value; }
        @Override public int hashCode() { 
            try {
                Thread.sleep(100);
            } catch (Exception e) {}
            return value.hashCode();
        }
    }

    final static Map<K, String> map = new HashMap<>();
    static {
        for (int i = 0; i < 1000; i++) {
            String s = Integer.toString(i);
            map.put(new K(s), s);
        }
    }

    public static void main(String[] args) {
        Runnable get = new Runnable() {
                @Override public void run() {
                    for (int i = 0; ; i++) {
                        if (i%1000 ==0) { System.out.printf("get: %d\n", i); }
                        Map<K, String> m2 = new HashMap<>(map);
                    }
                }
            };
        new Thread(get).start();
        for (int i = 0; ; i++) {
            if (i%1000 ==0) { System.out.printf("put: %d\n", i); }
            String s = Integer.toString(1000 + i);
            map.put(new K(s), s);
        }
    }
}

答案 4 :(得分:0)

在迭代时修改失败快速集合时会抛出

ConcurrentModificationException。实现这一目标的最简单方法是:

ArrayList<String> list = new ArrayList<String>();
// put items in list
for(String s : list)
   list.remove(0);

答案 5 :(得分:0)

如果您确实要创建ConcurrentModificationException,则需要Iterator

通过调用myMap.entrySet().iterator()myMap.keySet().iterator()myMap.values().iterator()明确地创建它,或者通过迭代&#34; foreach&#34;中的条目,键或值来隐式创建它。环。在迭代时,修改地图。

当某个东西在迭代时修改了一个集合时会抛出ConcurrentModificationException,否则会导致迭代器中出现未定义的行为。来自链接的Javadocs:

  

检测到并发的方法可能抛出此异常   当不允许这种修改时修改对象。   例如,一个线程通常不允许修改   一个集合,而另一个线程正在迭代它。一般来说,   在这些情况下,迭代的结果是不确定的。   一些迭代器实现(包括所有通用的实现)   由JRE提供的目的集合实现可以选择   如果检测到此行为,则抛出此异常。那个迭代器   这被称为故障快速迭代器,因为它们很快就会失败   干净利落,而不是冒着任意的,非确定性的行为冒险   未来不确定的时间。

     

请注意,此异常并不总是表示对象具有   由另一个线程同时修改。如果是单线程   发出一系列违反合同的方法调用   一个对象,该对象可能抛出此异常。例如,如果是   线程在迭代时直接修改集合   使用失败快速迭代器进行集合,迭代器将抛出此异常   异常。