概念
Least Recently Used(LRU),即最近最少使用页面置换算法。选择最长时间没有被引用的页面进行置换,思想是:如果一个页面很久没有被引用到,那么可以认为在将来该页面也很少被访问到。
在操作系统中的应用:当发生缺页(CPU要访问的页不在内存中),计算内存中每个页上一次被访问的时间,置换上次使用到当前时间最长的一个页面。
使用java可以有两种实现方式。
第一种:双向链表+哈希表
可以使用双向链表+哈希表的方式。HashMap主要是为了判断是否命中缓存。LinkedList用于维护一个按最近一次访问时间排序的页面链表。链表头结点是最近刚刚访问过的页面,链表尾结点是最久未被访问的页面。访问内存时,若命中缓存,找到响应的页面,将其移动到链表头部,表示该页面是最近刚刚访问的。缺页时,将链表尾部的页面移除,同时新页面放到链表头。
该类有四个方法:
- moveToFirst():把该元素移动链表的头部
- removeLast():把链表尾部元素删除。
- get():同步获取元素,map未命中,返回null。map命中则获取,并调用moveToFirst。
- put():同步放入元素。如果map未命中,如果链表长度已经超过缓存的大小,移除链表尾部的元素,把元素放入链表的头部和map里。如果map命中,则moveToFirst把该元素移到链表的头。
1 | public class LRUCache<K, V> { |
第二种:LinkedHashMap
JDK其实已经实现了一种顺序存储的HashMap,我们可以直接使用。代码如下
1 | package other; |
LinkedHashMap原理
LinkedHashMap继承了HashMap,作为HashMap的亲儿子,它们有很多相似的地方。
构造方法
1 | public LinkedHashMap() { |
这里有一个重要的成员变量accessOrder。因LinkedHashMap存储数据是有序的,而且分为两种:插入顺序和访问顺序。accessOrder则表示是否按访问顺序存储。这里设置为false,表示是插入顺序存储的,这也是默认值。
在上面代码中,构造函数的最后一个参数就是把accessOrder设置为true,表示按访问顺序排序
1 | map = new LinkedHashMap<K, V>(capacity, hashLoadFactory, true) |
Entry
LinkedHashMap与HashMap最大的不同在于它的Entry有前后指针—— Entry<K,V> before, after
1 | /** |
此外,在初始化的时候除了初始化了一个Entry[] table,还会额外初始化一个hash=-1,key、value、next都为null的只有头结点的双向链表,如图
1 |
|
put元素
当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中。
首先是只加入一个元素Entry1,假设index为0:
LinkedHashMap结构一个元素.png
当再加入一个元素Entry2,假设index为15:
LinkedHashMap结构两个元素.png
双向链表的重排序
当key如果已经存在时,并且accessOrder为true,即是访问顺序模式,才会put时对更新的Entry进行重新排序,而如果是插入顺序模式时,不会重新排序,这里的排序跟在HashMap中存储没有关系,只是指在双向链表中的顺序。
举个栗子:开始时,HashMap中有Entry1、Entry2、Entry3,并设置LinkedHashMap为访问顺序,则更新Entry1时,会先把Entry1从双向链表中删除,然后再把Entry1加入到双向链表的表尾,而Entry1在HashMap结构中的存储位置没有变化。
最后,有一个面试问题是如何实现顺序存储的hashmap,看完这篇文章,可以轻松回答了:可以使用双向链表加哈希表,也可以参照LinkedHashMap实现原理。