LinkedHashMap 源码分析

it2023-05-09  81

为啥要说到这个呢,因为它是实现LRU算法的比较方便的类,同时它的实现也依赖hashmap,而hashmap是Map里面使用最频繁的类。所以分析它非常有意义。 我们可以从LRU算法角度来分析LMM的基础功能。首先温习下LRU淘汰算法: lru缓存淘汰算法:该算法师根据数据的历史访问记录来进行淘汰数据,思想,如果该数据最近倍访问过,那么将来的访问率也比较高 实现思想:如果该数据已经存在我们的链表中,经过一次遍历,找到到该元素,需要将该数据放到链表的头部 如果该数据不在链表中,此时链表有足够的空间,将该数据放到链表的头部即可,如果该链表的数据已经满了,则删除链表尾的数据.,这腾出存储空间,将该数据放到链表的头部,总之lru核心思想是将最近最少使用的数据放到链表的尾部,如果存储空间不足的时候,淘汰即可,链表的头部总是存放最近使用率最频繁的数据.(转载自:https://blog.csdn.net/w5201314ws6123/article/details/85806213) 那么我们先从初始化来分析吧。

public LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) { //直接调用HashMap的构造方法。 super(initialCapacity, loadFactor); init(); //accessOrder 如果LRU 算法的话,应该为true。 this.accessOrder = accessOrder; }

init() { header = new LinkedEntry<K, V>(); } 初始头部header节点也就是root节点。这是一个双向循环链表,拥有nxt,prev,并且继承自HashMapEntry。 那么我们从put,get,还有迭代方法来分析LinkedHashMap的核心。 JDK 1.8中并没有put的方法是调用的HashMap的put,里面会addNewEntry。所以LLM重写了此方法。

void addNewEntry(K key, V value, int hash, int index) { LinkedEntry<K, V> header = this.header; //注意此处,意思是否需要将header.nxt 指向的节点删除,如果不是初始化的header,eldest!=header 会成立。但是removeEldestEntry 这个需要实现LRU算法的子类来实现。其实就是判断,目前的size是否太大,需要删除header.nxt,以便链表能够插入新的Entry. // Remove eldest entry if instructed to do so. LinkedEntry<K, V> eldest = header.nxt; if (eldest != header && removeEldestEntry(eldest)) { remove(eldest.key); } // Create new entry, link it on to list, and put it into table LinkedEntry<K, V> oldTail = header.prv; LinkedEntry<K, V> newTail = new LinkedEntry<K,V>( key, value, hash, table[index], header, oldTail); //此处实际上是将newTail插入到header头部。由于是循环链表,old6Tail.nxt= newTail。实际上LRU算法说简单点就是根据访问循序来的,但是实际上插入顺序也是默认的执行。 table[index] = oldTail.nxt = header.prv = newTail; }

现在看看这个get方法。

public V get(Object key) { /* * This method is overridden to eliminate the need for a polymorphic * invocation in superclass at the expense of code duplication. */ if (key == null) { HashMapEntry<K, V> e = entryForNullKey; if (e == null) return null; if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { K eKey = e.key; if (eKey == key || (e.hash == hash && key.equals(eKey))) { if (accessOrder) makeTail((LinkedEntry<K, V>) e); return e.value; } } return null; }

核心实际就是 makeTail((LinkedEntry<K, V>) e);。这个方法会将get到的LinkedEntry 移动到head头部。看方法。

private void makeTail(LinkedEntry<K, V> e) { //首先将e的unlik,实际就是先删除e。 // Unlink e e.prv.nxt = e.nxt; e.nxt.prv = e.prv; //然后将e放入到头部。这个在put里面的算法一样就不分析了。 // Relink e as tail LinkedEntry<K, V> header = this.header; LinkedEntry<K, V> oldTail = header.prv; e.nxt = header; e.prv = oldTail; oldTail.nxt = header.prv = e; modCount++; }

大家可以看到如果accessOrder 为true,get的时候会将get到的值放入头部这么做实际就是LRU算法。也可以叫做缓存算法。这样做的目的其实就是因为缓存容量优先,我们需要把最少使用的item放置到末尾,然后进行删除,这样cache维护的都是一些访问比较频繁的数据。这样当我们需要使用缓存的时候,首先可以直接到cache里面get,由于缓存存储的都是一些教频繁的数据,我们有一定的概率能够get到,那么如果命中那么就直接拿到对象了,不用再new了。这就是LRU算法思想,android开发里面大量使用了这种思想。而LinkedHashMap就是非常方便的实现这种算法的了。

最新回复(0)