• LinkedHashMap源码及LRU实现原理


    基本认识

     

    LinkedHashMap位于java.util包,于JDK1.4引入,属于Java Collections Framework的成员。查看其UML关系如下图所示:LinkedHashMap.png
    HashMap在很多场景下都满足K-V的存取,而且在非多线程的场景下也能保持不错的性能,但是HashMap有一个缺点就是不能保证元素的顺序,相比之下,LinkedHashMap就满足了这点,可以保证元素的插入顺序。
    LinkedHashMap的数据结构实现如下图所示:

    linkedhashmap数据结构.png


    LinkedHashMap也是Java中多态思想的一个重要运用,其底层实现可以看作是HashMapLinkedList的结合,使用HashMap来存储数据,使用LinkedList来维护链表的顺序,一定程度上也增加了时间和空间的消耗。

    源码解析

    相关变量

    在LinkedHashMap中有一个重要的成员变量,用来标志元素的排序规则,默认为false。

    1. /**
    2. * 双向链表的头部(最老的)。
    3. */
    4. transient LinkedHashMap.Entry head;
    5. /**
    6. * 双向链表的尾部(最小的)。
    7. */
    8. transient LinkedHashMap.Entry tail;
    9. /**
    10. * 用来指定LinkedHashMap的迭代排序方法:访问顺序为true ,插入顺序为false。
    11. */
    12. final boolean accessOrder;

    LinkedHashMap中的节点继承自HashMap.Node,采用的是双向链表的结构,因此支持双向遍历。

    1. static class Entry extends HashMap.Node {
    2. Entry before, after;
    3. Entry(int hash, K key, V value, Node next) {
    4. super(hash, key, value, next);
    5. }
    6. }

    构造方法

    LinkedHashMap共有5个构造方法,在不指定成员变量accessOrder作为构造入参时,都被显式的声明为false,这也说明了在默认情况下,LinkedHashMap是按照插入顺序进行排序。
    由于继承自HashMap,因此构造方法入参也可以指定初始容量initialCapacity和加载因子loadFactor

    1. public LinkedHashMap(int initialCapacity, float loadFactor) {
    2. super(initialCapacity, loadFactor);
    3. accessOrder = false;
    4. }
    5. public LinkedHashMap(int initialCapacity) {
    6. super(initialCapacity);
    7. accessOrder = false;
    8. }
    9. public LinkedHashMap() {
    10. super();
    11. accessOrder = false;
    12. }
    13. public LinkedHashMap(Map m) {
    14. super();
    15. accessOrder = false;
    16. putMapEntries(m, false);
    17. }
    18. public LinkedHashMap(int initialCapacity,
    19. float loadFactor,
    20. boolean accessOrder) {
    21. super(initialCapacity, loadFactor);
    22. this.accessOrder = accessOrder;
    23. }

    实现原理

    HashMap的底层数据结构为哈希表,这种结构的特点为数组+链表或者是数组+红黑树的结构,初始化容量指定的为数组的容量,每一个数组的下标位置被称为一个bucket,每个bucket处的头节点之后为链表或者红黑树,在进行元素查找时可以达到O(1)、O(logN)、O(N)的时间复杂度,具有较高的性能。
    LinkedHashMap在添加元素时直接调用的时HashMap的put实现,在HashMap中预留了对节点的后置操作回调函数,如下所示:

    1. // Callbacks to allow LinkedHashMap post-actions
    2. void afterNodeAccess(Node p) { }
    3. void afterNodeInsertion(boolean evict) { }
    4. void afterNodeRemoval(Node p) { }

    查看HashMap中put方法的源码实现:

    1. public V put(K key, V value) {
    2. return putVal(hash(key), key, value, false, true);
    3. }
    4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    5. boolean evict) {
    6. // ......省略片段
    7. Node[] tab; Node p; int n, i;
    8. if ((tab = table) == null || (n = tab.length) == 0)
    9. n = (tab = resize()).length;
    10. if ((p = tab[i = (n - 1) & hash]) == null)
    11. tab[i] = newNode(hash, key, value, null);
    12. else {
    13. // ......省略片段
    14. if (e != null) { // existing mapping for key
    15. V oldValue = e.value;
    16. if (!onlyIfAbsent || oldValue == null)
    17. e.value = value;
    18. // 对链表的节点进行后置增强操作
    19. afterNodeAccess(e);
    20. return oldValue;
    21. }
    22. }
    23. // 在节点插入之后所做的操作,HashMap中该方法未有任何实现,在LinkedashMap中
    24. // 实现了该方法,用于处理节点添加后的后置操作,evict该参数默认为true,是否驱逐节点
    25. afterNodeInsertion(evict);
    26. return null;
    27. }
    28. // HashMap在链表操作中调用了afterNodeAccess方法,用来在添加元素节点后进行,节点顺序排列
    29. void afterNodeAccess(Node e) { // move node to last
    30. LinkedHashMap.Entry last;
    31. // 如果accessOrder为true并且当前元素不是链表的尾结点,将当前节点移动至链表尾部
    32. if (accessOrder && (last = tail) != e) {
    33. LinkedHashMap.Entry p =
    34. (LinkedHashMap.Entry)e, b = p.before, a = p.after;
    35. // ......省略片段,链表操作
    36. }
    37. }

    查看afterNodeInsertion方法的实现,源码如下:

    1. // 该方法的作用是在新的元素插入后,可能会移除旧的节点
    2. // 因此LinkedHashMap很适合用来做LRU Cache
    3. void afterNodeInsertion(boolean evict) { // possibly remove eldest
    4. LinkedHashMap.Entry first;
    5. // evict默认为true,removeEldestEntry在LinkedHashMap中的实现默认为false
    6. // 在LinkedHashMap中默认不会移除,但是预留了扩展来实现缓存淘汰方案
    7. if (evict && (first = head) != null && removeEldestEntry(first)) {
    8. K key = first.key;
    9. // 移除添加时间最久的节点
    10. removeNode(hash(key), key, null, false, true);
    11. }
    12. }
    13. // 该方法是一个可以被继承实现的方法
    14. protected boolean removeEldestEntry(Map.Entry eldest) {
    15. return false;
    16. }

    关于节点的删除操作,查看removeNode方法的具体实现

    1. // 移除节点 movable为true,matchValue为false
    2. final Node removeNode(int hash, Object key, Object value,
    3. boolean matchValue, boolean movable) {
    4. Node[] tab; Node p; int n, index;
    5. if ((tab = table) != null && (n = tab.length) > 0 &&
    6. //......省略
    7. if (node != null && (!matchValue || (v = node.value) == value ||
    8. (value != null && value.equals(v)))) {
    9. //......省略
    10. // LinkedHashMap实现,移除节点后的回调函数
    11. afterNodeRemoval(node);
    12. return node;
    13. }
    14. }
    15. return null;
    16. }

    查看节点移除后的后置操作

    1. // 移除节点后
    2. void afterNodeRemoval(Node e) { // unlink
    3. // 临时节点p记录当前节点,a指向p的后一个节点,b指向p的前一个节点
    4. LinkedHashMap.Entry p =
    5. (LinkedHashMap.Entry)e, b = p.before, a = p.after;
    6. // 当前节点移除后,p的前一个、后一个节点都指向null
    7. p.before = p.after = null;
    8. // 处理head指向
    9. // 如果前一个结点为null,当前节点被移除,所以head指向当前节点的后一个节点
    10. if (b == null)
    11. head = a;
    12. else
    13. // 如果前一个节点不为null,当前节点被移除后,当前节点上一个节点指向下一个节点即可
    14. b.after = a;
    15. // 处理tail指向
    16. // 如果当前节点的下一个节点为null,说明当前节点为头节点,当前节点被移除,tail指向下一个
    17. if (a == null)
    18. tail = b;
    19. else
    20. // 当前节点的下一个节点不为null,直接进行节点追加
    21. a.before = b;
    22. }

    LRU实现

    在操作系统中,物理内存不可能同时加载所需的所有数据页,只能加载正在或最近要使用的内存页,这也就伴随着页面的更换,页面更换的目标是,尽量替换掉不再使用或者一段时间内不再使用的内存页,要不然会很容易触发缺页中断,该操作代价较大,涉及到从磁盘加载,因此页面更换可不是随便的事情。在内存不够的场景下,会进行内存淘汰的机制,随之也出现了各种淘汰算法,大致可以分为以下两大类:

    • 公平算法:随机算法、FIFO算法、时钟算法。
    • 非公平算法:NRU算法、LRU算法、工作集算法。

    LRU即Least Recently Used,最近最少使用算法,当缓存满了,会优先淘汰那些最近最不常访问的数据。在Java中利用LinkedHashMap可以很容易的实现LRU,在一些开源软件方案中也随处可见基于LinkedHashMap实现的LRU方案,如下面两个代码案例:

    1. /**
    2. * MySQL Driver LRU实现
    3. */
    4. public class LRUCache extends LinkedHashMap {
    5. private static final long serialVersionUID = 1L;
    6. protected int maxElements;
    7. public LRUCache(int maxSize) {
    8. super(maxSize, 0.75F, true);
    9. this.maxElements = maxSize;
    10. }
    11. protected boolean removeEldestEntry(Entry eldest) {
    12. return this.size() > this.maxElements;
    13. }
    14. }
    15. /**
    16. * Druid LRU实现
    17. */
    18. public class LRUCache extends LinkedHashMap {
    19. private static final long serialVersionUID = 1L;
    20. private final int maxSize;
    21. public LRUCache(int maxSize) {
    22. this(maxSize, 16, 0.75F, false);
    23. }
    24. public LRUCache(int maxSize, int initialCapacity, float loadFactor,
    25. boolean accessOrder) {
    26. super(initialCapacity, loadFactor, accessOrder);
    27. this.maxSize = maxSize;
    28. }
    29. protected boolean removeEldestEntry(Entry eldest) {
    30. return this.size() > this.maxSize;
    31. }
    32. }

    以上代码可以看出,实现的代码大同小异。下面使用具体案例来演示使用的过程:

    1. import java.util.LinkedHashMap;
    2. import java.util.Map.Entry;
    3. public class LRUCache extends LinkedHashMap {
    4. private static final long serialVersionUID = 1L;
    5. protected int maxElements;
    6. public LRUCache(int maxSize) {
    7. super(maxSize, 0.75F, true);
    8. this.maxElements = maxSize;
    9. }
    10. protected boolean removeEldestEntry(Entry eldest) {
    11. // 当HashMap的size大于预设的最大容纳元素数量maxElements时移除旧的元素
    12. return this.size() > this.maxElements;
    13. }
    14. public static void main(String[] args) {
    15. LRUCache cache = new LRUCache<>(3);
    16. cache.put("k1", 1);
    17. cache.put("k2", 2);
    18. cache.put("k3", 3);
    19. print(cache);
    20. cache.get("k3");
    21. cache.get("k2");
    22. cache.get("k1");
    23. print(cache);
    24. cache.put("k4",4);
    25. print(cache);
    26. }
    27. public static void print(LRUCache cache) {
    28. cache.forEach((k, v) -> System.out.println(k + " : " + v));
    29. }
    30. }

    输出结果:

    1. k1 : 1
    2. k2 : 2
    3. k3 : 3
    4. k3 : 3
    5. k2 : 2
    6. k1 : 1
    7. k2 : 2
    8. k1 : 1
    9. k4 : 4

    可以看到Cache中最大容纳元素数量为设定数量3,超过3的元素会被丢弃,缓存中的数据也是按照访问顺序进行排序,访问顺序包含put、get两种操作。其工作原理如下图所示:linkedhashmap - LRU (1).png

    总结

    • HashMap利用哈希表进行数据存取,其底层实现结构为数组+链表或者数组+红黑树,LinkedHashMap在使用HashMap进行数据存储的过程中,又维护了一个双向链表来维持顺序,通过accessOrder支持链表的双向遍历。
    • 基于LinkedHashMap可以巧妙的实现缓存LRU。既可以按照访问顺序,也可以按照插入顺序进行数据的淘汰。
    • 相比HashMap的访问操作,put、get方法,LinkedHashMap在进行元素访问后都会调用回调函数afterNodeAccess进行元素排序操作,在put元素后还会额外调用afterNodeInsertion回调方法进行后续操作,并且根据removeEldestEntry方法的返回结果判定是否进行旧元素移除,默认不进行删除。
  • 相关阅读:
    英语学习:M开头
    四 软件架构风格、质量属性、Web架构设计
    eKuiper Newsletter 2022-07|v1.6.0:Flow 编排 + 更好用的 SQL,轻松表达业务逻辑
    国际结算期末模拟试题A及参考答案
    Android 系统865虚拟化集成无源码apk示例
    飞行机器人专栏(八)-- AGX Xavier 通信、控制及视觉应用开发
    LeetCode·32.最长有效括号·栈·动态规划
    Java spring boot 一次调用多个请求
    纯js实现html指定页面导出word
    同行评议论文怎么写
  • 原文地址:https://blog.csdn.net/qq_38721537/article/details/126606786