LRUCache源码学习

本文深入探讨了LRU Cache的数据结构,包括其基于hash map和doubly linked list的实现,以及Android中的LRUCache类。文章详细解析了get和put方法的工作原理,以及如何通过trimToSize方法实现缓存淘汰策略,确保线程安全和动态调整容量。
摘要由CSDN通过智能技术生成

一、LRU Cache数据结构

LRU Cache

LRU(Least Recently Used)含义是最近最少使用(也即是最久未使用),一种Cache替换算法。
狭义的Cache指的是位于CPU和主存间的快速RAM。广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。

  Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。

数据结构

  LRU的典型实现是hash map + doubly linked list。
如果没有哈希表,我们要访问某个结点,就需要顺序地一个个找, 时间复杂度是O(n)。使用哈希表可以让我们在O(1)的时间找到想要访问的结点,或者返回未找到。
双向链表用于存储数据结点,并且它是按照结点最近被使用的时间来存储的。 如果一个结点被访问了,我们把它放到双向链表的头部。当我们往双向链表里插入一个结点,同样把它插入到头部。 我们使用这种方式不断地调整着双向链表,链表尾部的结点自然也就是最久没有使用到的结点。那么,当我们的Cache满了, 需要替换掉的就是双向链表中最后的那个结点(不是尾结点,头尾结点不存储实际内容)。
如下是双向链表示意图:
头 —> 结—> 结 —> 结 —> 尾
结——点——点——点——结
点 <— 1 <— 2 <— 3 <— 点

二、Android LRUCache

谷歌在Android3.1(v4 library也支持)之后提供了一个封装此数据结构实现的LRUCache类。它用强引用保存需要缓存的对象,它内部维护一个队列(实际上是LinkedHashMap实现),当其中的一个值被访问时,它被放到队列的前面,当缓存将满时,队列尾部的值(也就是最近最少使用的)被丢弃,之后可以被垃圾回收。
LinkedHashMap集合除了拥有HashMap的大部分特性之外,还拥有链表的特点,即可以保持遍历顺序与插入顺序一致。也可以使遍历顺序和访问顺序一致,其内部双向链表将会按照近期最多访问到近期最少访问的顺序排列Entry对象。
下面开始分析LruCache(android.util包下)。

package android.util;  
import java.util.LinkedHashMap;  
import java.util.Map;  
public class LruCache<K, V> {  

首先是这个类的成员变量:

private final LinkedHashMap<K, V> map;  
    /** Size of this cache in units. Not necessarily the number of elements. */  
    private int size; //已经存储的大小
    private int maxSize; //规定的最大存储空间

    private int putCount;  //put的次数
    private int createCount;  //create的次数
    private int evictionCount;  //回收的次数
    private int hitCount;  //命中的次数
    private int missCount;  //丢失的次数

LinkedHashMap的初始化放在构造器中:

public LruCache(int maxSize) {  
        if (maxSize <= 0) {  
            throw new IllegalArgumentException("maxSize <= 0");  
        }  
        this.maxSize = maxSize;  
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);  
    }  

这里将LinkedHashMap的accessOrder设置为true。
接下来看两个最重要的方法,put和get。首先是get方法:

public final V get(K key) {  
        if (key == null) {      //不允许空键
            throw new NullPointerException("key == null");  
        }  

        V mapValue;  
        synchronized (this) {  //线程安全  
            mapValue = map.get(key);    //调用LinkedHashMap的get方法
            if (mapValue != null) {  
                hitCount++;  //命中次数加1  
                return mapValue;  //返回value
            }  
            missCount++;  //丢失, //未命中 
        }  

        V createdValue = create(key);  //默认返回为false 
        if (createdValue == null) {  
            return null;  
        }  

        synchronized (this) {  
            createCount++;//如果创建成功,那么create次数加1    
            mapValue = map.put(key, createdValue);  //放到哈希表中

            if (mapValue != null) {  
                //如果前面存在oldValue,那么撤销put() 
                map.put(key, mapValue);  
            } else {  
                size += safeSizeOf(key, createdValue);  
            }  
        }  

        if (mapValue != null) {  
            entryRemoved(false, key, createdValue, mapValue);  
            return mapValue;  
        } else {  
            trimToSize(maxSize);  
            return createdValue;  
        }  
    }  

get方法即通过key返回相应的V,或者创建返回相应的V。相应的entry会移动到队列的头部,如果entry的value没有被cache或者不能被创建,则返回null。
接下来是put方法:

public final V put(K key, V value) {  
        if (key == null || value == null) {  //键值不允许为空
            throw new NullPointerException("key == null || value == null");  
        }  

        V previous;  
        synchronized (this) {  //线程安全  
            putCount++;  
            size += safeSizeOf(key, value);  
            previous = map.put(key, value);  
            if (previous != null) {  //之前已经插入过相同的key 
                size -= safeSizeOf(key, previous); //那么减去该entry的容量,因为发生覆盖
            }  
        }  

        if (previous != null) {  
            entryRemoved(false, key, previous, value);  //这个方法默认空实现 
        } 
        trimToSize(maxSize);  //若容量超过maxsize,将会删除最近很少访问的entry
        return previous;  
    }  

put方法调用LinkedHashMap的put方法,但是这里在调用LinkedHashMap的put方法之前,判断了key和value是否为空,也就是说LruCache不允许空键值。除此之外,put操作被加锁了,所以是线程安全的!
既然是缓存,那么必然能够动态删除一些不常用的键值对,这个工作是由trimToSize方法完成的:

 private void trimToSize(int maxSize) {  
        while (true) {  //不断删除linkedHashMap尾部entry,也就是最近最少访问的条目,直到size小于最大容量  
            K key;  
            V value;  
            synchronized (this) {  //线程安全
                if (size < 0 || (map.isEmpty() && size != 0)) {  
                    throw new IllegalStateException(getClass().getName()  
                            + ".sizeOf() is reporting inconsistent results!");  
                }  

                if (size <= maxSize) {  //直到容量小于最大容量为止 
                    break;  
                }  

                Map.Entry<K, V> toEvict = map.eldest();  //指向链表尾
                if (toEvict == null) {  
                    break;  
                }  

                key = toEvict.getKey();  
                value = toEvict.getValue();  
                map.remove(key);  //删除最少访问的entry  
                size -= safeSizeOf(key, value);  
                evictionCount++;  
            }  

            entryRemoved(true, key, value, null);  
        }  
    }  

这个方法不断循环删除链表尾部元素,也就是最近最少访问的元素,直到容量不超过预先定义的最大值为止。
注:LruCache在support.v4包中也有一个LruCache类,但是这个类的trimToSize方法是
不断循环删除链表首部元素,也就是近访问最多的元素,这是不正确的!
以上就是LruCache最重要的部分,下面再看下其他方法:

remove:
    public final V remove(K key) {  
        if (key == null) {  
            throw new NullPointerException("key == null");  
        }  

        V previous;  
        synchronized (this) {  
            previous = map.remove(key);  //调用LinkedHashMap的remove方法  
            if (previous != null) {  
                size -= safeSizeOf(key, previous);  
            }  
        }  

        if (previous != null) {  
            entryRemoved(false, key, previous, null);  
        }  

        return previous;  //返回value  
    }  

sizeof:这个方法用于计算每个条目的大小,子类必须得复写这个类。

protected int sizeOf(K key, V value) {  //用于计算每个条目的大小  
        return 1;  
    }  
  snapshot方法,返回当前缓存中所有的条目集合
    public synchronized final Map<K, V> snapshot() {  
        return new LinkedHashMap<K, V>(map);  
    }  
…………
…………
}

总结:

get(K)或者put(K),它们始终都会将K对应的V移到链表头部,这样链表尾部就成为了最久没有使用的数据结点。remove(K)用来删除数据,通过调用entryRemoved(boolean evicted, K key, V oldValue, V newValue)达到进一步的数据清理。
1.LruCache封装了LinkedHashMap,提供了LRU缓存的功能;
2.LruCache通过trimToSize方法自动删除最近最少访问的键值对;
3.LruCache不允许空键值;
4.LruCache线程安全;
5.继承LruCache时,必须要复写sizeof方法,用于计算每个条目的大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值