庖丁解牛之LruCache 源代码分析和设计

背景

LruCache这个很常见,做过android的开发没见过也都听说过这个,一般应用常见就是做缓存的时候用到,说一下我与LruCache的故事吧,好多年面试的时候让我实现一下Lru算法,我当时用HashMap搞的,搞的好复杂,其实你看Android的中LruCahe类很简单,就三百多行代码

什么是Lru算法呢?

LRU是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法,是为虚拟页式存储管理服务的。

android中是如何实现的呢?

 */
public class LruCache {
    private final LinkedHashMap map;
    //缓存的数据大小
    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(0, 0.75f, true);
    }
}

别看上面的代码很简单,关键的就是LinkedHashMap,这个是实现LRU算法的关键,所有的功能都是基于LinkedHashMap实现的

this.map = new LinkedHashMap(0, 0.75f, true);

这个代码很关键,LinkedHashMap最后一个参数true很关键,这个true决定了LinkedHashMap遍历的访问顺序,

true表示迭代的时候安装访问顺序迭代,false表示访问的时候安装插入书序迭代,为了解释这个问题我们先上一段代码

/**
 * @author nate
 * @since 2018/8/6.
 */
public class Test {


    public static void main(String args[]) {
        LinkedHashMap linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("1", "1");
        linkedHashMap.put("2", "2");
        linkedHashMap.put("3", "3");
        linkedHashMap.put("4", "4");

        linkedHashMap.get("1");
        linkedHashMap.get("2");
        linkedHashMap.get("2");

        for (String s : linkedHashMap.keySet()) {
            System.out.println(s);
        }
    }
}

输出的结果是:

1
2
3
4

即便我们代码中访问两次key为2的数据,对输出结果没有任何影响,这个输出的结果就是安装插入顺序迭代的

调整一下代码,我们再创建LinkHashMap的时候制定按照访问顺序排序访问

public static void main(String args[]) {
    LinkedHashMap linkedHashMap = new LinkedHashMap(0,0.75f,true);
    linkedHashMap.put("1", "1");
    linkedHashMap.put("2", "2");
    linkedHashMap.put("3", "3");
    linkedHashMap.put("4", "4");

    linkedHashMap.get("1");
    linkedHashMap.get("2");
    linkedHashMap.get("2");

    for (String s : linkedHashMap.keySet()) {
        System.out.println(s);
    }
}

输出结果:

3
4
1
2

这个地方比较关键的是我们访问了两次key为2的数据,意思key为2的数据是最后的迭代的,没有访问过的数据是先迭代的,这就是LRU的关键

我们分析一下get代码

public final V get(K key) {
    //对key进行非空判断
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        //首先获取map中的数据
        mapValue = map.get(key);
        //获取到数据
        if (mapValue != null) {
            //命中数加一
            hitCount++;
            return mapValue;
        }
        //获取不到数据 丢失数++
        missCount++;
    }

    /*
     * Attempt to create a value. This may take a long time, and the map
     * may be different when create() returns. If a conflicting value was
     * added to the map while create() was working, we leave that value in
     * the map and release the created value.
     */
    //试图去创建数据,默认的实现是返回null的
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        //创建数量+1
        createCount++;
        //放到缓存中
        mapValue = map.put(key, createdValue);

        //如果缓存中已经有数据了,执行撤销操作
        if (mapValue != null) {
            // There was a conflict so undo that last 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;
    }
}

总结一下:

  • 首先在缓存中查找数据,如果有数据就返回,hitCount++
  • 如果找不到,就看是否实现了create 方法,默认是空实现的
  • 如果实现了create 方法,先把创建的数据放到缓存中,如果缓存已经有了数据,则执行撤销操作,并且要回调entryRemoved 执行释放操作
  • 计算一下总体的数据大小

如何移除无用的数据呢?

移除数据的操作主要是在put方法中回调的,如果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) {
        //put数量++
        putCount++;
        //计算总数据大小
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        //之前已经存储过数据了
        if (previous != null) {
            //减去之前数据的大小
            size -= safeSizeOf(key, previous);
        }
    }
    //回调移除数据放
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    //计算存储的数据是否超标了
    trimToSize(maxSize);
    return previous;
}

计算超过最大值移除逻辑

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!");
            }

            //如果缓存的数据<=maxSize return
            if (size <= maxSize) {
                break;
            }
            //移除无用的代码
            Map.Entry 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);
    }
}

获取相关记录的数据

public synchronized final int maxSize() {
    return maxSize;
}

/**
 * Returns the number of times {@link #get} returned a value that was
 * already present in the cache.
 */
public synchronized final int hitCount() {
    return hitCount;
}

/**
 * Returns the number of times {@link #get} returned null or required a new
 * value to be created.
 */
public synchronized final int missCount() {
    return missCount;
}

/**
 * Returns the number of times {@link #create(Object)} returned a value.
 */
public synchronized final int createCount() {
    return createCount;
}

/**
 * Returns the number of times {@link #put} was called.
 */
public synchronized final int putCount() {
    return putCount;
}

/**
 * Returns the number of values that have been evicted.
 */
public synchronized final int evictionCount() {
    return evictionCount;
}

/**

统计所有的数据

@Override public synchronized final String toString() {
    int accesses = hitCount + missCount;
    int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
    return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
            maxSize, hitCount, missCount, hitPercent);
}

总结

  • LruCache 本身的源代码也是比较简单的,主要逻辑的都LinkedHashMap 实现的,搞懂LinkedHashMap也就搞懂了LruCache

你可能感兴趣的:(庖丁解牛之LruCache 源代码分析和设计)