LRU Cache理解

LRU Cache

1. 概念解析,LRU Cache算法

  1. Lru Cache算法就是Least Recently Used,按照字面意思就是最近最少使用,用这种算法来实现缓存就比较合适了,当缓存满了的时候,不经常使用的就直接删除,挪出空间来缓存新的对象;
  2. 实现缓存的最关键的操作就是,添加和读取以及删除等操作了
  3. LRU 实现使用LinkedHashMap永久的缓存数据,那为什么要用这个呢?
    1. LinkedHashMap是双向列表实现的,刚好在内部具有排序的功能,内部的accessorder代表了两种模式,插入模式和访问模式,false为访问模式按照顺序来实现(默认就是false),所以按照此种思路,则链表的最后段就是最少使用的缓存,比较方便来实现;
    2. LinkedHashMap是双向循环列表来实现,默认容量大小16、负载因子0.75以及按照插入顺序排序,不用我们管理扩容等问题;
    3. 添加和读取数据:保证访问顺序排序,会将数据插入或者移动到链表的尾部,而且链表的删除和增加速度比较快;
    4. LinkedHashMap遍历顺序是从头到尾,这样可以保证删除最老的数据;

2. LRU cache的简单使用

    int maxMemory = (int) (Runtime.getRuntime().totalMemory()/1024);
    int cacheSize = maxMemory/8;
    mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getRowBytes()*value.getHeight()/1024;
        }
    };
  1. 获取到当前虚拟机的最大内存值,然后取1/8来当缓存;
  2. 注意单位的一致性sizeof()和cacheSize的单位要一直,上面为kb;
  3. sizeOf()是为了计算缓存对象大小的计算;
  4. 使用的时候你就可以当做一个map去使用就好了,只不过自动添加了扩容,缓存,以及帮你防止OOM的情况;

3. LRU Cache源码解析

  1. 分析源码主要从几个方面来分析,创建,存取,删除这三个方面来:

  2. 创建:

     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;
     private int createCount;
     private int evictionCount;
     private int hitCount;
     private int missCount;
    
    
     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;

  3. put方法是把内容放入到缓存中去

     //给对应key缓存value,该value将被移动到队头。
     public final V put(K key, V value) {
      //不可为空,否则抛出异常
     if (key == null || value == null) {
         throw new NullPointerException("key == null || value == null");
     }
     V previous;
     synchronized (this) {
         //插入的缓存对象值加1,记录 put 的次数
         putCount++;
         //增加已有缓存的大小,拿到键值对,计算出在容量中的相对长度,然后加上
         size += safeSizeOf(key, value);
        //向map中加入缓存对象,如果 之前存在key 则返回 之前key 的value,记录在 previous
         previous = map.put(key, value);
         //如果已有缓存对象,则缓存大小恢复到之前
         if (previous != null) {
             //   // 计算出 冲突键值 在容量中的相对长度,然后减去
             size -= safeSizeOf(key, previous);
         }
     }
     //entryRemoved()是个空方法,可以自行实现,如果上面发生冲突
     if (previous != null) {
     //previous值被剔除了,此次添加的 value 已经作为key的 新值,告诉 自定义 的 entryRemoved 方法
         entryRemoved(false, key, previous, value);
     }
     //调整缓存大小
     trimToSize(maxSize);
     return previous;
     }
    
    
      /*
      * 这是一个死循环,
      * 1.只有 扩容 的情况下能立即跳出
      * 2.非扩容的情况下,map的数据会一个一个删除,直到map里没有值了,就会跳出
      */
    
     public void trimToSize(int maxSize) {
             while (true) {
                 K key;
                 V value;
                 synchronized (this) {
                      // 在重新调整容量大小前,本身容量就为空的话,会出异常的。
                     //如果map为空并且缓存size不等于0或者缓存size小于0,抛出异常
                     if (size < 0 || (map.isEmpty() && size != 0)) {
                         throw new IllegalStateException(getClass().getName()
                                 + ".sizeOf() is reporting inconsistent results!");
                     }
                     // 如果是 扩容 或者 map为空了,就会中断,因为扩容不会涉及到丢弃数据的情况
                     //如果缓存大小size小于最大缓存,或者map为空,不需要再删除缓存对象,跳出循环
                     if (size <= maxSize || map.isEmpty()) {
                         break;
                     }
                     //迭代器获取第一个对象,即队尾的元素,近期最少访问的元素
                     Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                     key = toEvict.getKey();
                     value = toEvict.getValue();
                     //删除该对象,并更新缓存大小
                     map.remove(key);
                      // 拿到键值对,计算出在容量中的相对长度,然后减去。
                     size -= safeSizeOf(key, value);
                     evictionCount++;
                 }
                     //将最后一次删除的最少访问数据回调出去
                 entryRemoved(true, key, value, null);
             }
         }
    

    put方法比较简单只是把对象存储,然后关键的方法是trimToSize(),调整缓存的,如果满了就删除然后更新

  4. get获取缓存

    public final V get(K key) {
    //key为空抛出异常
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        //获取对应的缓存对象
        //get()方法会实现将访问的元素更新到队列头部的功能,LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }

    void recordAccess(HashMap<K,V> m) {
                LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
                //判断是否是访问排序
                if (lm.accessOrder) {
                    lm.modCount++;
                    //删除此元素
                    remove();
                    //将此元素移动到队列的头部
                    addBefore(lm.header);
                }
            }
  1. 总结:
    1. LRUcache的源码相对简单,只要理解LinkedHashMap的原理,这个是非常简单的实现;关键代码是trimSize方法,每次添加完成之后调整缓存大小,get方法的也是调用的LinkedHashMap的get然后通过recordAcess来调整顺序;

带注释的LruCache
LruCache原理和用法与LinkedHashMap
LruCache 解析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容