LRUCache原理

前言

LRU及Least Recently Used, 最近最少使用算法, 也就是当内存缓存达到设定的最大值时将内存缓存中近期最少使用的对象移除,有效的避免了OOM的出现.
上篇文章我们看了LinkedHashMap的源码, 通过LinkedHashMap可以很方便的获取最少使用的元素, 而LRUCache也就是通过LinkedHashMap来实现的, 下面来看下LRUCache的代码.

源码

  • 注释
/**
 * A cache that holds strong references to a limited number of values. Each time
 * a value is accessed, it is moved to the head of a queue. When a value is
 * added to a full cache, the value at the end of that queue is evicted and may
 * become eligible for garbage collection.
 *
 * <p>If your cached values hold resources that need to be explicitly released,
 * override {@link #entryRemoved}.
 *
 * <p>If a cache miss should be computed on demand for the corresponding keys,
 * override {@link #create}. This simplifies the calling code, allowing it to
 * assume a value will always be returned, even when there's a cache miss.
 *
 * <p>By default, the cache size is measured in the number of entries. Override
 * {@link #sizeOf} to size the cache in different units. For example, this cache
 * is limited to 4MiB of bitmaps:
 * <pre>   {@code
 *   int cacheSize = 4 * 1024 * 1024; // 4MiB
 *   LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 *       protected int sizeOf(String key, Bitmap value) {
 *           return value.getByteCount();
 *       }
 *   }}</pre>
 *
 * <p>This class is thread-safe. Perform multiple cache operations atomically by
 * synchronizing on the cache: <pre>   {@code
 *   synchronized (cache) {
 *     if (cache.get(key) == null) {
 *         cache.put(key, value);
 *     }
 *   }}</pre>
 *
 * <p>This class does not allow null to be used as a key or value. A return
 * value of null from {@link #get}, {@link #put} or {@link #remove} is
 * unambiguous: the key was not in the cache.
 *
 * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
 * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
 * Support Package</a> for earlier releases.
 */

总结一下注释中提到的几点信息:

  1. LRUCache是一个持有有限数据的强引用. 每次访问元素, 该元素就会被移到队列头部. 队列满了之后再次添加元素, 就会回收队列尾部的元素.
  2. 如果缓存的值持有的资源需要被彻底释放, 需要重写entryRemoved进行释放操作.
  3. 重写create方法, 这样缓存即使丢失也总能被返回.
  4. 默认的缓存占用大小是缓存对象的个数, 需要重写sizeOf计算不同缓存对象所占的大小.
  5. LRUCache是线程安全的.
  6. 不允许使用空的key和value.
  7. 出现在Android3.1, 也在Android Support包中兼容早期版本.
  • 变量及构造方法
public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;

    private int size;//内存占用大小
    private int maxSize;//缓存最大容量

    private int putCount;//put的次数
    private int createCount;//create的次数
    private int evictionCount;//回收的次数
    private int hitCount;//get到的次数
    private int missCount;//未get到的次数

    //构造函数中传入最大缓存容量
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        //初始化LinkedHashMap, 扩充因子为0.75, accessOrder为true, 按访问顺序排列, 方便获取最老的元素
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
}
  • put
public final V put(K key, V value) {
        //key和value都不能为空
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);//size累加该对象缓存大小
            previous = map.put(key, value);//获取key对应的之前的值
            //不为空, size要减去之前的值所占的缓存大小
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        //key之前对应的有值, 通知旧值被替换
        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }
        //重新计算是否超过最大容量
        trimToSize(maxSize);
        return previous;
    }
private int safeSizeOf(K key, V value) {
        //获取该对象缓存大小
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
//需要重写该方法, 设置每个缓存对象的大小, 默认为1
protected int sizeOf(K key, V value) {
        return 1;
    }
//当entry对象被回收或删除或替换时, 通过此方法回调出去
//evicted: 是否被回收   oldValue: key对应的旧值   newValue:key对应的新值
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
}

LRUCache的put操作就是在LinkedHashMap的put操作上增加了缓存大小的计算和是否超出最大容量的计算, 如果超出就删除最旧的缓存对象.

  • trimToSize
    计算缓存是否超出最大值.
public void trimToSize(int maxSize) {
        while (true) {
            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;
                }
                //缓存容量超出最大值之后, 通过LinkedHashMap获取最旧的元素
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);// 删除最旧的元素
                size -= safeSizeOf(key, value);//重新计算缓存大小
                evictionCount++;//增加回收次数
            }
            //回调通知回收了最旧的元素
            entryRemoved(true, key, value, null);
        }
    }
  • get
public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            //map获取到key对应的值之后直接返回
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        //如果map中没有获取到, 判断用户是否重写create来创建对象
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }
        //如果用户重写了create创建对象
        synchronized (this) {
            createCount++;
            //把key和新创建的对象存入map, 并获取key对应的之前的对象
            mapValue = map.put(key, createdValue);
           //如果key对应的之前的对象不为空, 再重新把之前的值存入map
           //因为create可能需要一段时间, 多线程访问时可能会有key对应旧值不为空的情况, 所以保存旧值, 放弃新值
            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {//key对应旧值为空, 累加缓存大小
                size += safeSizeOf(key, createdValue);
            }
        }
        //key对应旧值不为空, 缓存大小不不变, 通知删除新值, 返回旧值
        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {//key没有对应旧值, 重新计算是否超出最大值, 返回新值
            trimToSize(maxSize);
            return createdValue;
        }
    }

get就是通过LinkedHashMap获取key对应的value, 有则直接返回, 没有则创建, 如果创建了新value就加入LinkedHashMap, 重新计算缓存大小, 重新计算是否超出最大缓存.

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

        V previous;
        synchronized (this) {
            //删除key对应的value
            previous = map.remove(key);
            //key对应value不为空, 删除之后要重新计算缓存大小
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }
        //key对应value不为空, 删除之后, 通知删除了该value
        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }

        return previous;
    }

remove操作也很简单, 通过LinkedHashMap删除key对应value之后重新计算缓存大小.

  • resize
public void resize(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }

        synchronized (this) {
            this.maxSize = maxSize;
        }
        trimToSize(maxSize);
    }

很简单, 就是重新设置缓存最大值, 重新计算当前缓存是否超出最大值.

基本操作就这些, 是不是很简单, 存取都是通过LinkedHashMap来操作的, 只不过多了缓存大小的计算, 以及是否超出最大缓存的计算, 如果超出最大缓存, 就把最少使用的对象删除.

原理清楚了, 具体使用可以参考大神的照片墙

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

推荐阅读更多精彩内容