与LinkedHashMap的ConcurrentModificationException

时间:2013-04-23 23:00:11

标签: java concurrentmodification linkedhashmap

当我在下面的代码中迭代java.util.ConcurrentModificationException结构时,不确定触发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();

    }

}

6 个答案:

答案 0 :(得分:41)

阅读Javadoc for LinkedHashMap

  

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

由于您将true传递给LinkedHashMap构造函数,因此它处于访问顺序,当您尝试从get构建某些内容时,您正在对其进行结构修改。< / p>

另请注意,使用增强的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是一个自动生成的标识符,不同于其中的任何其他标识符(自动生成或以其他方式)   范围(第6.3节)在增强的声明发生时。

另外,在LinkedHashMap的Javadoc中:

  

集合的iterator方法返回的迭代器   所有这个类的集合视图方法都返回了    fail-fast :如果在创建迭代器之后的任何时候对地图进行结构修改,除了通过迭代器自己以外的任何方式   remove方法,迭代器会抛出一个   ConcurrentModificationException

因此,当您在地图上调用get时,您正在对它进行结构修改,导致enhanced-for中的迭代器抛出异常。我认为你打算这样做,这可以避免调用get

for (Integer i : lru_cache.values()) {
    System.out.println(i);
}

答案 1 :(得分:8)

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

  

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

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

答案 2 :(得分:4)

LinkedHashMap的构造函数中,您传递true以获取LRU行为(意味着驱逐策略为访问顺序而非<{1}} 广告订单)。

因此,每次调用false时,底层Map.Entry会增加一个访问计数器,并通过将(最后访问的)Map.Entry移动到列表的头部来重新排序集合。

迭代器(由for循环隐式创建)检查修改后的标志,该标志与最初使用的副本不同,因此抛出get(key)

为了避免这种情况,您应该使用ConcurrentModificationException,因为实现是从java.util.HashMap继承的,因此迭代器不检查修改标志:

entrySet()

请注意,此类不是线程安全的,因此在并发环境中,您需要使用像Collections.synchronizedMap(Map)这样的潜在昂贵的防护。在这种情况下,更好的选择可能是Google's Guava Cache

答案 3 :(得分:2)

java.util.ConcurrentModificationException:如果迭代器存在,则对基础列表进行任何结构更改(添加,删除,重新散列等)。迭代器在每次操作之前检查列表是否已更改。这被称为“故障安全操作”。

如果线程在使用失败快速迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。在使用迭代器时,您无法调用get()方法,因为调用{{ 1}}从结构上修改了映射,因此对其中一个迭代器方法的下一次调用失败并抛出get()

答案 4 :(得分:1)

您的代码

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缓存会在调用set()时将自身缩小为MAX_SIZE元素,但在调用get()时 - 上述答案解释原因。

因此我们有以下行为:

  • 创建新迭代器以迭代lru_cache.keySet()集合
  • lru_cache.get()被调用以从缓存中提取元素
  • get()调用将lru_cache截断为MAX_SIZE个元素(在您的情况下为3)
  • 迭代器it由于集合修改而变为无效,并在下一次迭代时抛出。

答案 5 :(得分:1)

当您修改列表(通过添加或删除元素)时,它也是集合框架的快速失败行为,而遍历具有此错误的列表将与Iterator一起使用。一段时间后我偶然发现了这个错误。有关详细信息,请参阅以下主题。

ConcurrentModificationException when adding inside a foreach loop in ArrayList

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

Concurrent Modification Exception : adding to an ArrayList

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