LinkedHashMap与LRU——来自源码的启示

LinkedHashMap是什么?

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap是HashMap的扩展,它根据元素的插入顺序或者访问顺序(accessOrderd属性指定),使用双向链表,将所有元素连接起来,使得对HashMap的遍历变得有序。
示意图如下:
LinkedHashMap示意图
(图片引用自:https://blog.csdn.net/justloveyou_/article/details/71713781)

为什么要设计这个类?

这个实现是为了解决HashMap和HashTable无序问题,而又不增加像TreeMap那样对树操作的高额成本。

LinkedHashMap与LRU有什么关系?

LRU是一种缓存淘汰算法,LRU(Least recently used,最远使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。如果对LRU不清楚的同学可以参考这篇文章:https://blog.csdn.net/zhoucheng05_13/article/details/79829601

下面是一段LinkedHashMap源码的注释文档:

<p>A special {@link #LinkedHashMap(int,float,boolean) constructor} is
 * provided to create a linked hash map whose order of iteration is the order
 * in which its entries were last accessed, from least-recently accessed to
 * most-recently (<i>access-order</i>).  This kind of map is well-suited to
 * building LRU caches. 

上面源码注释的意思是,LinkedHashMap的一个特殊的构造器LinkedHashMap(int,float,boolean)被用来创建一个从最远到最近被访问的访问顺序排序的LinkedHashMap。这样的map非常适合用于实现LRU缓存。

LinkedHashMap是怎么实现LRU的呢?

1. 通过构造器指定accessOrder为true

上面提到的构造器源码如下:

 /**
     * Constructs an empty {@code LinkedHashMap} instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - {@code true} for
     *         access-order, {@code false} for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

重点在于它设置了accessOrder属性,关于该属性的定义如下:

/**
     * 这个属性定义了迭代器的遍历顺序,若为true,则使用访问顺序(LRU),若为false,则使用
     * 插入顺序。该属性是final类型的常量,只能赋值一次。
     * 该属性默认情况下为false。
     * The iteration ordering method for this linked hash map: {@code true}
     * for access-order, {@code false} for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;
2. 在元素被访问后将其移动到链表的末尾

在元素被访问时,如果accessOrder为true,即该map通过访问顺序排序,那么被访问的元素会被移动到链表的末尾,以get方法为例:

 public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)   //这里是重点!
            afterNodeAccess(e);
        return e.value;
    }

可以看到,在指定的key存在的情况下,会判断accessOrder的属性,如果为true,会调用afterNodeAccess(e)方法,该方法源码如下:

//当Node被访问后,将其移动到链表的最后
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

哪些操作算访问操作?

这个在源码官方文档中同样有详细的说明:

 Invoking the {@code put}, {@code putIfAbsent},
 * {@code get}, {@code getOrDefault}, {@code compute}, {@code computeIfAbsent},
 * {@code computeIfPresent}, or {@code merge} methods results
 * in an access to the corresponding entry (assuming it exists after the
 * invocation completes). The {@code replace} methods only result in an access
 * of the entry if the value is replaced.  The {@code putAll} method generates one
 * entry access for each mapping in the specified map, in the order that
 * key-value mappings are provided by the specified map's entry set iterator.
 * <i>No other methods generate entry accesses.</i>  In particular, operations
 * on collection-views do <i>not</i> affect the order of iteration of the
 * backing map.

上面的文档可以分为几个点:

  1. 调用put、putIfAbsent、get、getOrDefault、compute、computeIfAbsent、computeIfPresent、merge方法会引起对相应Entry的访问。
  2. replace方法只有在value被替换时才算访问操作
  3. putAll方法对传入map中的每一个映射都产生了一次访问(根据map的Iterator顺序)。

除此以外,没有任何方法会产生对entry的访问。尤其需要注意的是!在集合视图中的操作不会影响背后Map的迭代顺序。
上面的规则都可以从源码中找打答案,以replace()方法为例(该方法在HashMap中):

 @Override
    public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);  //注意这里!
            return oldValue;
        }
        return null;
    }

从上面我们可以看到,只有当key存在,并对value进行了替换之后,才会调用afterNodeAccess(e); 方法,产生顺序的调整。
所以,归根结底,只有在源码中调用了afterNodeAccess(e);方法,才会调整节点顺序,即算作对entry的访问操作。

LinkedHashMap对LRU还提供了那些支持?

LinkedHashMap对LRU策略青睐有加,它专门为其设计了一个方法:

    /*    @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns {@code true}.  If the map was empty prior
     *           to the {@code put} or {@code putAll} invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   {@code true} if the eldest entry should be removed
     *           from the map; {@code false} if it should be retained.
     */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

该方法的说明文档长达20余行,为了节省篇幅,这里就不贴出来了,下面我会对其一一说明。
首先,这个方法的作用是决定当有新元素插入时,是否要移除Eldest的元素。它的调用时机是在新元素被插入到map中后,被put和putAll方法调用。文档中这样说道:“它为实现者提供了一个机会,可以在每次添加新条目时删除最老的条目。这在使用map做缓存时非常有用:它允许map通过删除最旧的元素来减少内存消耗。”下面是官方文档中举的一个简单例子:

       private static final int MAX_ENTRIES = 100;

       protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > MAX_ENTRIES;
        }

这个例子的作用是,当map增长到100时,每次插入新的元素就删除一个最老的元素,使得容量始终保持在100.
这个方法没有直接修改map,而是通过返回值决定是否允许map修改自身。要在这个方法中直接修改map也是允许的,不过如果要这么做,那么必须返回false,以防止map被重复修改。
在默认的实现中,这个方法仅仅返回false,所以这个map表现的像一个普通的map,最老的元素永远不会被移除。如果需要实现特定的功能,我们需要向下面这样重写该方法:

LinkedHashMap map = new LinkedHashMap(){
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size()>100;
            }
        };

总结

常听说Map可以用于实现缓存,当阅读了这个类过后才有了直观的感受。从源码中的方法可以看出,该类的确是为缓存量身定制的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值