LRU——LinkedListMap的实现原理

简介: LRU是Least Recently Used的缩写,即最近最少使用,当超过容量时,自动删除最近最少使用的项目。LRU在android开发中最常见的就是图片加载框架中的缓存逻辑。在java中可以利用LinkedListMap很方便的实现LRU

前言


LRU是Least Recently Used的缩写,即最近最少使用,当超过容量时,自动删除最近最少使用的项目。

LRU在android开发中最常见的就是图片加载框架中的缓存逻辑。在java中可以利用LinkedListMap很方便的实现LRU,如下


class LRUCache extends LinkedHashMap<Integer, Integer> {
    //LinkedHashMap实现了按访问排序和移除最近最少,但是默认是不使用的,需要我们改造一下
    private int capacity;
    public LRUCache(int capacity) {
        //这里的第三个参数true就是使用访问排序,这样每次访问都是更新链表
        //注意,这里的第一个参数是map的初始大小,并不是限定容量
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    public void put(int key, int value) {
        super.put(key, value);
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        //这个函数默认返回false,如果不重写这个方法,永远不会进行清除
        //所以这里要重写一下,判断是否进行清除。
        return size() > capacity;
    }
}
复制代码


那么这其中的原理是什么呢?


原理


首先就是LinkedHashMap,它与HashMap的区别就是在Map的结构外还有一个链表结构,Map的每个key-value都会以Node的形式同时存储在Map和链表中。

当put一个key和value时,加入Map的同时,也加入链表尾部,这样实现了有序(HashMap本来是无序的)。


这里就不展开说了,简单知道有这个链表就可以了,正是这个链表完成了LRU的实现。

简单来说就是get(使用了)的时候将该Node从链表中移除,并添加到链表尾部。

put的时候,检查Map的容量,如果超出了,就从移除链表头部Node(时间最久的,即最近最少使用),同时从map中移除。


那么代码中是如何实现的?

先看get函数,这个函数LinkedHashMap进行了重写:


public V get(Object var1) {
    Node var2;
    if ((var2 = this.getNode(hash(var1), var1)) == null) {
        return null;
    } else {
        if (this.accessOrder) {
            this.afterNodeAccess(var2);
        }
        return var2.value;
    }
}
public V getOrDefault(Object var1, V var2) {
    Node var3;
    if ((var3 = this.getNode(hash(var1), var1)) == null) {
        return var2;
    } else {
        if (this.accessOrder) {
            this.afterNodeAccess(var3);
        }
        return var3.value;
    }
}
复制代码


可以看到有这么一段代码:


if (this.accessOrder) {
    this.afterNodeAccess(var3);
}
复制代码


这里的accessOrder就是LinkedHashMap的三参数构造函数super(capacity, 0.75F, true)中的第三个参数,是否使用访问排序。

默认是false,如果为ture就使用访问排序,这时候get函数就会调用afterNodeAccess,这个函数是HashMap的,但是在HashMap中是个空函数,没有任何实现:


void afterNodeAccess(HashMap.Node<K, V> var1) {
}
复制代码


所以如果使用HashMap,这个访问排序就没有什么意义。但是在LinkedHashMap中对这个函数进行了重写:


void afterNodeAccess(Node<K, V> var1) {
    LinkedHashMap.Entry var2;
    if (this.accessOrder && (var2 = this.tail) != var1) {
        LinkedHashMap.Entry var3 = (LinkedHashMap.Entry)var1;
        LinkedHashMap.Entry var4 = var3.before;
        LinkedHashMap.Entry var5 = var3.after;
        var3.after = null;
        if (var4 == null) {
            this.head = var5;
        } else {
            var4.after = var5;
        }
        if (var5 != null) {
            var5.before = var4;
        } else {
            var2 = var4;
        }
        if (var2 == null) {
            this.head = var3;
        } else {
            var3.before = var2;
            var2.after = var3;
        }
        this.tail = var3;
        ++this.modCount;
    }
}
复制代码


简单来说就是将get函数访问的这个Node从链表中移除,添加到结尾。 所以可以看到,如果想实现LRU,accessOrder就必须为true,否则无效。


自动清理


这样实现了访问排序,那么如何实现的自动清理呢?

我们来看看put函数,put函数LinkedHashMap没有重写,所以在它的父类HashMap中:


public V put(K var1, V var2) {
    return this.putVal(hash(var1), var1, var2, false, true);
}
final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
    HashMap.Node[] var6;
    int var8;
    if ((var6 = this.table) == null || (var8 = var6.length) == 0) {
        var8 = (var6 = this.resize()).length;
    }
    Object var7;
    int var9;
    if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
        var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
    } else {
        ...
        if (var10 != null) {
            Object var13 = ((HashMap.Node)var10).value;
            if (!var4 || var13 == null) {
                ((HashMap.Node)var10).value = var3;
            }
            this.afterNodeAccess((HashMap.Node)var10);
            return var13;
        }
    }
    ++this.modCount;
    if (++this.size > this.threshold) {
        this.resize();
    }
    this.afterNodeInsertion(var5);
    return null;
}
复制代码


这里重点关注链表的逻辑,可以看到如果Map中不存在这个key,就新建一个


if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
    var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
} else {
复制代码


但是如果存在,除了重新赋值之外,还执行了afterNodeAccess,将它移到了链表尾部。


if (var10 != null) {
    ...
    this.afterNodeAccess((HashMap.Node)var10);
    return var13;
}
复制代码


因为重新赋值就是访问了该元素,所以需要重新排序。

那么这里受不受accessOrder的控制,因为accessOrder是LinkedHashMap的一个变量,而put是HashMap实现的,所以在put的代码中并没有判断accessOrder。但是在上面afterNodeAccess函数的源码中可以看到一开始就判断了accessOrder,所以这里也是受accessOrder的控制的。这也是为什么在get函数中判断了accessOrder,在afterNodeAccess又一次判断的原因。


最后还执行了afterNodeInsertion,这个函数与afterNodeAccess一样,在HashMap中是空函数,没有任何代码。在LinkedHashMap实现了,如下:


void afterNodeInsertion(boolean var1) {
    LinkedHashMap.Entry var2;
    if (var1 && (var2 = this.head) != null && this.removeEldestEntry(var2)) {
        Object var3 = var2.key;
        this.removeNode(hash(var3), var3, (Object)null, false, true);
    }
}
复制代码


可以看到这个函数实现了移除Map中的元素,这样就实现了清理。但是并不是每次都清理,所以需要removeEldestEntry判断。

这个函数是LinkedHashMap的函数,但是在LinkedHashMap中这个函数也是一个空函数,默认返回false


protected boolean removeEldestEntry(java.util.Map.Entry<K, V> var1) {
    return false;
}
复制代码


所以永远不会清理,这也是我们为什么一定要重写这个函数的原因。通过我们上面的重写,当Map容量大于我们规定的上限时就返回true,这样就执行了清理。


@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
    //这个函数默认返回false,如果不重写这个方法,永远不会进行清除
    //所以这里要重写一下,判断是否进行清除。
    return size() > capacity;
}
复制代码


总结


所以要用LinkedHashMap实现LRU,有两个非常重要的点:


  • 1、accessOrder必须为true。否则链表不会重排。

注意:如果为false,还是可以清理的(下面会说到),这时候如果清理实际上就是清理存入最久的,也就相当于一个普通的队列。


  • 2、必须重写removeEldestEntry。否则永远不会清理。

那么上面提到的一个问题,清理是否受accessOrder影响?是不是accessOrder为false就不清理了?


答案是不影响,重排和清理是互不影响的,在afterNodeInsertion的整个流程中没有accessOrder的出现。

所以上面提到了,如果accessOrder为false也可以清理,只不过清理的是存入最久的(忽略访问),并不是LRU算法。


目录
相关文章
|
11月前
|
人工智能 JSON 安全
大模型应用开发中MCP与Function Call的关系与区别
MCP与Function Call是大模型应用的两大关键技术。前者为跨模型工具调用的标准化协议,实现系统解耦与生态扩展;后者是模型调用外部功能的内置机制。二者互补协同,推动AI应用向高效、开放、安全演进。
|
存储 缓存 安全
Java集合框架(三):Map体系与ConcurrentHashMap
本文深入解析Java中Map接口体系及其实现类,包括HashMap、ConcurrentHashMap等的工作原理与线程安全机制。内容涵盖哈希冲突解决、扩容策略、并发优化,以及不同Map实现的适用场景,助你掌握高并发编程核心技巧。
|
设计模式 缓存 JavaScript
API设计模式:REST、GraphQL、gRPC与tRPC全面解析
API设计模式:REST、GraphQL、gRPC与tRPC全面解析
1106 0
|
存储 关系型数据库 MySQL
MySQL - 聚簇索引和非聚簇索引
MySQL - 聚簇索引和非聚簇索引
1088 0
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
689 4
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
常用工具类-Arrays
本文介绍了Java工具类Arrays和Arrays.ArrayList的常用功能,包括数组的打印、创建、比较、排序、检索以及转换为List的方法,以及setAll和parallelSetAll的使用,展示了如何通过这些工具类便捷地操作数组。
常用工具类-Arrays
|
API 开发者
币安合约现货策略交易接口API对接开发源代码详情
# 生成签名的函数(示例) def generate_signature(params, secret): signature = '' for key in sorted(params.keys()):
|
机器学习/深度学习 人工智能 Python
|
JSON Linux API
币安(binance)API接口
原文链接 币安API Rest-API 2018-01-04 基本信息 URL: https://api.binance.com JSON协议 数据默认按ASC(升序)排序,时间早的在上面 所有和时间有关的单位都是毫秒 HTTP 4XX : 请...
12897 1
|
安全 Java
ConcurrentHashMap是如何保证线程安全的
总的来说,ConcurrentHashMap通过分段锁和红黑树等策略,实现了既能保证线程安全,又能保持高并发性能的特性。
594 1

热门文章

最新文章