JAVA LinkedHashMap与LRU,LinkedHashMap keySet遍历Map失败之谜

来源:互联网 发布:淘宝联盟怎么弄优惠券 编辑:程序博客网 时间:2024/06/06 03:53

最近看图片缓存的时候看到了LinkedHashMap,继承于HashMap(差别百度一大堆),于是仔细看了一下,这里先介绍LinkedHashMap怎么实现LRU的,在介绍使用keySet来遍历LinkedHashMap(以下简称“LHM”)为什么会出现java.util.ConcurrentModificationException错误。
1.LRU(Least Recently Used 近期最少使用)的实现,在LHM的文档里面有一句话“This kind of map is well-suited to building LRU caches”,那么它是如何维护LRU的序列的呢?首先注意到它有一个构造方法

public LinkedHashMap(int initialCapacity,                         float loadFactor,                         boolean accessOrder)

这里第三个参数是关键,如果为true表明排列方式为访问顺序,false表明为插入顺序。客户端将其设为true就能实现LRU了,但LHM具体是怎么来实现的呢?这里的实现肯定与get有关,于是看看get,下面列出了HashMap.get与LHM.get的源码:

    public V get(Object key) {//LHM的get        Entry<K,V> e = (Entry<K,V>)getEntry(key);        if (e == null)            return null;        e.recordAccess(this);        return e.value;    }     public V get(Object key) {//HashMap的get        if (key == null)            return getForNullKey();        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue();    }

这里可以注意到LRU的关键实现就是recordAccess这个方法。源码为:

 void recordAccess(HashMap<K,V> m) {            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;            if (lm.accessOrder) {                lm.modCount++;                remove();                addBefore(lm.header);            }        }

里面有两个关键方法remove和addBefore,从名字可以看出移除这个元素并添加到尾部。LHM的entry是一个双向链表,这也就是为什么LHM为什么可以保持put进去的顺序,这里大家可以想象这两个方法如何实现的,这里就不贴代码了,注意在remove方法前面的modCount++,这句话是造就后面异常的关键。
2.LHM遍历出现异常。(前提是构造方法第三个参数为true)
我这里用了两个方法遍历,一个是用entrySet(成功),一个是用keySet(失败,出现ConcurrentModificationException错误),显然这两个方法是在HashMap是可以成功遍历的。这里贴一下代码和异常,方便大家分析:

LinkedHashMap<String, String> map=new LinkedHashMap<>(10, 0.75f, true);        map.put("1", "1");        map.put("2", "2");        map.put("3", "3");        Iterator<String> it = map.keySet().iterator();        System.out.println(map.keySet().getClass()+" "+it.getClass());        while ( it.hasNext()) {            System.out.println(map.get(it.next()));        }//输出class java.util.HashMap$KeySet class java.util.LinkedHashMap$KeyIteratorException in thread "main" 1java.util.ConcurrentModificationException    at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(Unknown Source)    at java.util.LinkedHashMap$KeyIterator.next(Unknown Source)    at access.Main.main(Main.java:19)

根据异常情况可看到在LinkedHashIterator.nextEntry里面出现了异常,这方法里面有个判断:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
这个expectedModCount初始化为modCount,那么这个异常是如何抛出的呢?大家应该能想到我上面说的get里面有个modCount++,这遍历时调用了get,导致两者不等,于是抛出了异常。现在还有一个问题,这个nextEntry是如何触发的呢?它的上一层是LinkedHashMap$KeyIterator.next,这里请大家结合代码看看输出的第一行,可以得出LMH在keySet方法后得到了HashMap的keySet对象,这keySet方法是LHM继承HashMap的,没有覆盖,得到HashMap的keySet不奇怪,后面再调用iterator方法从HashMap的keySet得到了LinkedHashMap的KeyIterator就用点奇怪了,原因是HashMap的的keySet里面的iterator方法调用的HashMap的newKeyIterator方法,而这个方法被LHM覆盖了,于是返回LHM的KeyIterator,最后在调用get方法的时候就抛出了异常。这个设计真巧妙!!!
最后说一下这个ConcurrentModificationException,大家可以去百度,这里贴部分文档信息“This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
For example, it is not generally permissible for one thread to modify a Collection while another thread is iterating over it.”大概意思检查到了修改但修改是不允许的,如一个线程正在迭代一个集合,另一个线程通常是不允许去改变这个集合的。

0 0