20

当我在下面的代码中java.util.ConcurrentModificationException迭代结构时,不确定是什么触发了 a 。LinkedHashMap使用该Map.Entry方法效果很好。从之前的帖子中没有得到关于触发此问题的良好解释。

任何帮助,将不胜感激。

import java.util.LinkedHashMap;
import java.util.Map;

public class LRU {

    // private Map<String,Integer> m = new HashMap<String,Integer>();
    // private SortedMap<String,Integer> lru_cache = Collections.synchronizedSortedMap(new TreeMap<String, Integer>());

    private static final int MAX_SIZE = 3;

    private LinkedHashMap<String,Integer> lru_cache = new LinkedHashMap<String,Integer>(MAX_SIZE, 0.1F, true){
        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return(lru_cache.size() > MAX_SIZE);
         }
    };    

    public Integer get1(String s){
        return lru_cache.get(s);        
    }

    public void displayMap(){
        /**
         * Exception in thread "main" java.util.ConcurrentModificationException
            at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:373)
            at java.util.LinkedHashMap$KeyIterator.next(LinkedHashMap.java:384)
            at LRU.displayMap(LRU.java:23)
            at LRU.main(LRU.java:47)
         */
        *for(String key : lru_cache.keySet()){
            System.out.println(lru_cache.get(key));
        }*

// This parser works fine        
//        for(Map.Entry<String, Integer> kv : lru_cache.entrySet()){
//            System.out.println(kv.getKey() + ":" + kv.getValue());
//        }
    }

    public void set(String s, Integer val){
        if(lru_cache.containsKey(s)){            
            lru_cache.put(s, get1(s) + val);
        }
        else{
            lru_cache.put(s, val);
        }
    }

    public static void main(String[] args) {

        LRU lru = new LRU();
        lru.set("Di", 1);
        lru.set("Da", 1);
        lru.set("Daa", 1);
        lru.set("Di", 1);        
        lru.set("Di", 1);
        lru.set("Daa", 2);
        lru.set("Doo", 2);
        lru.set("Doo", 1);        
        lru.set("Sa", 2);
        lru.set("Na", 1);
        lru.set("Di", 1);
        lru.set("Daa", 1);

        lru.displayMap();

    }

}
4

6 回答 6

44

阅读JavadocLinkedHashMap

结构修改是添加或删除一个或多个映射的任何操作,或者在访问排序的链接哈希映射的情况下,影响迭代顺序。在插入排序的链接哈希映射中,仅更改与映射中已包含的键关联的值不是结构修改。在访问排序的链接哈希映射中,仅查询映射get是一种结构修改。

由于您正在传递trueLinkedHashMap构造函数,因此它处于访问顺序,当您尝试从中获取get某些内容时,您正在对其进行结构修改。

另请注意,当您使用增强for语法时,实际上是在使用迭代器。JLS §14.14.2的简化引用:

增强for语句具有以下形式:

EnhancedForStatement:

for ( TargetType Identifier : Expression ) Statement

[...]

如果Expression的类型是Iterable<X>某个类型参数的子类型X,则令I为类型java.util.Iterator<X>;否则,I设为原始类型java.util.Iterator

增强for语句等价于for以下形式的基本语句:

for (I #i = Expression.iterator(); #i.hasNext(); ) {
     TargetType Identifier =
         (TargetType) #i.next();
     Statement
}

#i是一个自动生成的标识符,与增强 for 语句发生时范围内(第 6.3 节)内的任何其他标识符(自动生成或以其他方式)不同。

此外,在 Javadoc 中LinkedHashMap

iterator由此类的所有集合视图方法返回的集合 的方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时间对映射进行结构修改,除了通过迭代器自己的 remove方法之外的任何方式,迭代器会抛出一个 ConcurrentModificationException.

因此,当您get在地图上调用时,您正在对其进行结构修改,从而导致增强型中的迭代器抛出异常。我认为您打算这样做,从而避免调用get

for (Integer i : lru_cache.values()) {
    System.out.println(i);
}
于 2013-04-23T23:23:30.967 回答
9

您正在使用按访问顺序链接的哈希映射:来自http://docs.oracle.com/javase/7/docs/api/java/util/LinkedHashMap.html的规范,

结构修改是添加或删除一个或多个映射的任何操作,或者在访问排序的链接哈希映射的情况下,影响迭代顺序。在插入排序的链接哈希映射中,仅更改与映射中已包含的键关联的值不是结构修改。在按访问顺序链接的哈希映射中,仅使用 get 查询映射是一种结构修改。)

简单的调用get就足以被认为是结构修改,触发了异常。如果您使用entrySet()序列,您只查询条目而不是地图,因此您不会触发ConcurrentModificationException.

于 2013-04-23T23:19:46.303 回答
6

In the constructor of LinkedHashMap you pass true to get the LRU behaviour (meaning the eviction policy is access order rather than false for insertion order).

So every time you call get(key) the underlying Map.Entry increments an access counter AND reorders the collection by moving the (last accessed) Map.Entry to the head of the list.

The iterator (implicitly created by the for loop) checks the modified flag, which is different from the copy it took originally, so throws the ConcurrentModificationException.

To avoid this you should use the entrySet() as the implementation is inherited from java.util.HashMap and therefore the iterator doesn't check the modification flags:

    for(Map.Entry<String,Integer> e : lru_cache.entrySet()){
        System.out.println(e.getValue());
    }

Be aware this class isn't threadsafe so in concurrent environments you will need to use an potentially expensive guards like Collections.synchronizedMap(Map). In this scenario a better option might be Google's Guava Cache.

于 2013-04-24T00:05:44.947 回答
2

你的代码

for(String key : lru_cache.keySet()){
    System.out.println(lru_cache.get(key));
}

实际上编译为:

Iterator<String> it = lru_cache.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    System.out.println(lru_cache.get(key));
}

接下来,您的 LRU 缓存不是在调用时收缩到 MAX_SIZE 元素set(),而是在调用时get()- 上面的答案解释了原因。

因此我们有以下行为:

  • 创建新的迭代器以迭代lru_cache.keySet()集合
  • lru_cache.get()调用以从缓存中提取元素
  • get()调用截断lru_cache为 MAX_SIZE 元素(在您的情况下为 3)
  • 由于集合修改,迭代器it变得无效并在下一次迭代中抛出。
于 2014-06-23T06:02:00.850 回答
2

java.util.ConcurrentModificationException:当迭代器存在时,如果底层列表有任何结构变化(添加、删除、重新散列等)。迭代器在每次操作之前检查列表是否已更改。这被称为“故障安全操作”。

如果线程在使用快速失败迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。在这里,您不能get()在使用迭代器时调用该方法,因为调用get()会在结构上修改映射,因此下一次调用其中一个迭代器方法失败并抛出一个ConcurrentModificationException.

于 2014-06-24T15:14:42.003 回答
1

当您修改列表(通过添加或删除元素)时,集合框架的快速失败行为也是因为在遍历列表时出现此错误,迭代器也会出现此错误。前段时间我遇到了这个错误。有关详细信息,请参阅下面的线程。

在 ArrayList 中的 foreach 循环内添加时出现 ConcurrentModificationException

虽然这表示数组列表,但它适用于大多数集合数据结构。

并发修改异常:添加到 ArrayList

http://docs.oracle.com/javase/6/docs/api/java/util/ConcurrentModificationException.html

于 2014-06-23T07:35:31.720 回答