LinkedHashMap详解及其在LruCache中的应用

在之前的文章红黑树在HashMap中的应用中,我们分析了HashMap的实现原理以及查找,插入和删除操作的源码,这一篇我们就来看看HashMap的一个子类:LinkedHashMap。

  • LinkedHashMap

    LinkedHashMap继承自HashMap并实现了Map接口,它的API与HashMap完全一致,用法也大致相同,同样是非线程安全的集合。我们 知道HashMap存储的节点都是HashMap.Node类型的,到了LinkedHashMap中,节点类型变成了LinkedHashMapEntry,这是Node的一 个子类,在Node类的基础上增加了before和after两个指针,用于双向链表的实现,下面代码是LinkedHashMapEntry类的定义:

    /**
    * HashMap.Node subclass for normal LinkedHashMap entries.
    */
    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

    由于这两个指针的存在,LinkedHashMap可以在保持HashMap存储结构不变的前提下,将持有的节点额外以双向链表的形式连接起来。这个 双向链表也是LinkedHashMap与其父类最大的不同,虽然存储方式完全相同,但是LinkedHashMap可以通过双向链表做到有序遍历,而这 个顺序取决于accessOrder这个成员变量。

    首先,老规矩,一起先来看看LinkedHashMap的全局成员变量:

        /**
        * The head (eldest) of the doubly linked list.
        */
        transient LinkedHashMapEntry<K,V> head; //指向双向链表头结点的指针
    
        /**
         * The tail (youngest) of the doubly linked list.
        */
        transient LinkedHashMapEntry<K,V> tail; //指向双向链表尾结点的指针
    
        /**
        * The iteration ordering method for this linked hash map: <tt>true</tt>
        * for access-order, <tt>false</tt> for insertion-order.
        *
        * @serial
        */
        //LinkedHashMap对于双向链表节点的顺序有两层维护方式。
        //第一层:按照节点插入顺序排序,先来先排
        //第二层:若accessOrder变量被置位true(在构造函数中传入),则每一次对节点的访问或修改都会将该节点移动到队尾
        //这样一来head指针指向的就是最久没有被访问的节点,这一个特性完全符合LRU(Least Recently Used)算法
        final boolean accessOrder; 

    OK,成员变量不多,作用也很直观。另外值得注意的是LinkedHashMap所重写的HashMap中留下的几个钩子方法,这些方法都是在 HashMap结构发生变化或节点被访问时被调用(例如put,get,remove),而HashMap中这些都是空方法,LinkedHashMap通过重写这 些方法实现了对节点双向链表结构的维护。下面就一起来看看这几个钩子方法的实现:

    //这个方法是在节点被访问或者被修改时被调用的
    void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMapEntry<K,V> last;
            if (accessOrder && (last = tail) != e) { //若accessOrder为false或e已经在链表尾部,则无需调整
                //暂存被操作节点e为p,其前驱节点为b,后继节点为a
                LinkedHashMapEntry<K,V> p =
                    (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
                p.after = null; //将p的后继置空
                if (b == null)
                    head = a; //若e的前驱节点为空,则说明e之前为链表的头结点,现在将e原先的后继结点变为头结点
                else
                    b.after = a; //前驱不为空,则连接e的前驱和后继节点,将e从链表中断开
                if (a != null)
                    a.before = b; //e原先的后继不为空,则与原先的前驱b连接
                else
                    last = b; //否则的话将尾指针指向b
                if (last == null)
                    head = p; //若尾指针此时为空,说明e是链表中唯一的节点,则将头指针重新指向它
                else {
                    p.before = last; //否则的话将其移到链表尾部
                    last.after = p;
                }
                tail = p; //尾指针指向尾节点
                ++modCount; //结构操作数加一
            }
    }
    
    //这个方法是在有新节点插入时被调用的
    //evict这个变量如果为true,说明需要在节点添加后进行一次eldest节点删除。反之则是单增长模式
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMapEntry<K,V> first;
        //如果不为单增长模式,链表不为空,且removeEldestEntry的返回值为true,就将头结点删除
        //值得注意的是removeEldestEntry方法默认的返回是false,即LinkedHashMap默认插入时不删除eldest节点
        //若需修改,则在子类中重写该方法
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true); //删除链表头结点
        }
    }
    
    //这个方法当有节点被删除时会被调用
    //e为待删除节点,这里做的也很简单,就是将这个已经被HashMap存储结构删除的节点从双向链表中移除
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

    上面这三个就是LinkedHashMap重写的其父类的三个钩子方法,除了这三个方法外,LinkedHashMap还重写了其他的一些HashMap的方 法,其主要目的有两个,一是将节点类从Node包装为LinkedHashMapEntry, 二是维护自身的双向链表。下面我们就一起来看下这几个被 重写的方法:

    //这里用LinkedHashMapEntry代替了HashMap.Node作为链表节点
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMapEntry<K,V> p = new LinkedHashMapEntry<K,V>(hash, key, value, e);
        linkNodeLast(p); //将新创建出的节点添加到链表尾部
        return p;
    }
    
    //这是构建桶中的红黑树结构时用到的新建节点的方法,这里相比HashMap会多做一步linkNodeLast,即将节点添加到链表尾部
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }
    
    //这个方法在untreeify过程中会被调用,作用就是将传入的TreeNode类型的p转换为普通的Node类型,当然这里是LinkedHashMapEntry
    //需要说明的是,红黑树的节点类型TreeNode其实是LinkedHashMapEntry的子类,所以这里直接进行了强转
    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMapEntry<K,V> q = (LinkedHashMapEntry<K,V>)p;
        LinkedHashMapEntry<K,V> t = new LinkedHashMapEntry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t); //这里是一个替换操作,用t替换原先链表中的q
        return t;
    }
    
    //这个方法会在treeifyBin方法中被调用,作用是将节点类型由LinkedHashMapEntry转换成TreeNode
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMapEntry<K,V> q = (LinkedHashMapEntry<K,V>)p;
        TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t); //与上相同,替换双向链表中的节点
        return t;
    }
    
    //LinkedHashMap还重写了父类中的get方法
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null) //前半部分跟HashMap完全一样,也是通过getNode方法完成
            return null;
        if (accessOrder) //若key匹配到了对应得value,且accessOrder为true
            afterNodeAccess(e); //通过afterNodeAccess方法将被访问节点移动到链表尾部
        return e.value;
    }

    可以看到,LinkedHashMap并没有改变任何HashMap中的存储结构和操作,只是将Node节点替换为了LinkedHashMapEntry节点,并且 在每一个操作方法后都会对自身的双向链表进行相应的维护。从这个角度看来,LinkedHashMap同时具有了Map和链表的特性,而这样的特 性使得它在很多地方都被应用,比如下面要出场的LruCache。

  • LruCache

    首先,LRU是Least Recently Used的缩写,也即最少使用算法。而LruCache,顾名思义就是基于LRU算法思想的缓存策略。它会将有限的缓存对象以强引用的方式保存,当其中某一个缓存对象被访问后,该对象就会被升级一次,而当队列已满且有新的添加请求时,最久没有被访问过的那个对象就会被移除出缓存队列,LruCache对其的强引用也会断开,以方便可能的GC。

    看到这里,是不是会有一种LinkedHashMap简直就是为了LRU算法而生的感觉��。。 言归正传,Android对于LruCache的实现确实就是基于LinkedHashMap完成的,上面所说的对象被访问后升级其实就是LinkedHashMap中afterNodeAccess方法所做的事情——将该节点移动至链表尾部,这样一来,链表的头结点必定就是最久没有被访问的对象,也即LruCache需要腾地方时要移除的对象。下面先一起来看下LruCache的构造函数:

    //maxSize是你所需要的缓存队列的容量
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        //重点在这里,map成员变量是一个LinkedHashMap实例,其初始容量为0,扩容因子为0.75
        //accessOrder为true,即节点被访问后需要调整位置到链表尾部
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    
    //顺带看一下这个扩容方法,与其他的集合类不同,LruCache并不存在自动扩容机制,这个resize方法也是由外界调用的
    //如果你需要更大或更小的缓存队列,可以调用这个public的resize方法
    public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
    
        synchronized (this) { //对象锁,线程安全
            this.maxSize = maxSize;
        }
        trimToSize(maxSize); //这个方法很重要,用于调整LruCache缓存队列的容量,下面会有详述
    }

    构造函数很简单,记录一下最大缓存容量,创建一个初始容量为0的LinkedHashMap。下面我们要重点来看看trimToSize这个内部方法,LruCache缓存队列容量的变化都是靠它完成的,put,get和resize等方法中都会对其进行适时的调用。

     //这里的输入参数maxSize指的是目标容量,方法体中的size变量是当前持有的缓存对象数
     public void trimToSize(int maxSize) {
        while (true) { //开启循环
            K key;
            V value;
    
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) { //size的合法性检查
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //若当前持有的缓存数量还未到设置的最大容量,则退出循环,结束
                if (size <= maxSize) {
                    break;
                }
    
                //这里用到了LinkedHashMap中的eldest方法,该方法会返回LinkedHashMap维护的双向链表的头结点
                //也即最久没有被访问过的节点。注意这个方法是被@hide注解的,且从注释来看,是专门为Android添加的
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break; //若取到的对象为空,即map为空,则没必要继续进行调整
                }
    
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key); //从map中删除之前取出的最老节点
                size -= safeSizeOf(key, value); //当前存储的节点数减一
                evictionCount++; //移除的缓存个数加一
            }
              //钩子方法,在这里可以对被移除的节点做一些事情,在LruCache中为空方法,由子类按需实现
            entryRemoved(true, key, value, null); 
        }
    }

    翻看LruCache的源码我们会发现它的put,get等操作其实都是通过LinkedHashMap类型的map变量来完成的,因此它对于缓存对象的存储结构与LinkedHashMap完全一致,并且得益于LinkedHashMap内部维护的双向链表轻松实现了LRU算法的思想。

    最后再提一句,LruCache中所有的public方法都含有对象锁,因此它是线程安全的。但是在高并发的情况下其性能不会很好,因为在一条线程获得锁时,不论它操作的是哪一个方法,该LruCache对象中的所有其他public方法都会被锁住而对其他线程不可用。

    感谢阅读!

版权声明:原创不易,转载前请留言获得作者许可,转载后标明作者 Troy.Tang 与 原文链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值