5

代码如下所示:这里使用的地图是 Guava 地图

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);在这个过程中,迭代器创建了一个副本,当迭代器工作时,如果地图的内容被修改,它就会不高兴。

4

6 回答 6

2

假设您确实使用com.google.common.collect.Maps,的实现newHashMap

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

如果我们再看一下实现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

于 2013-08-16T21:20:38.430 回答
1

ConcurrentModificationException在迭代 a Collectionor时抛出Map,使用它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(),随后的修复应该可以可靠地工作。

于 2013-08-16T21:04:55.993 回答
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); }
    }
}
于 2013-08-16T22:07:21.953 回答
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);
        }
    }
}
于 2013-08-16T22:54:38.153 回答
0

如果你真的想创建一个ConcurrentModificationException,你需要一个Iterator.

通过调用myMap.entrySet().iterator()myMap.keySet().iterator()或显式创建它myMap.values().iterator(),或者通过在“foreach”循环中迭代条目、键或值来隐式创建它。迭代时,修改地图。

AConcurrentModificationException在某个集合被迭代时被修改时抛出,否则会导致迭代器中的未定义行为。从链接的 Javadocs:

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

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

于 2013-08-16T21:05:25.063 回答
0

ConcurrentModificationException当快速失败的集合在迭代时被修改时抛出。实现这一点的最简单方法是例如:

ArrayList<String> list = new ArrayList<String>();
// put items in list
for(String s : list)
   list.remove(0);
于 2013-08-16T21:04:55.210 回答