LRU
什么是LRU
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。
实现思路
开始时,内存中没有页面。
每次访问页面时,先检测内存中是否存在该页面,若不存在则将该页面加载到内存“末尾”,若存在则直接访问该页面,并将该页面移到内存“末尾”。
如果访问某个内存中不存在的页面时,内存已满,则将内存“开头”的页面移出,并将新的页面加载到内存“末尾”。
这样就可以始终保持着最近访问的页面在不经常访问的页面的后面了。
数据结构的选择
数组
页面命中时,需要将其移动到数组末尾。页面命中失败时,需要删除数组头部元素,并将所有元素向前移动一步,然后在末尾添加元素。
这两个操作都涉及大量元素的移动,时间复杂度为O(n),效率较低。
链表
虽然不涉及到元素的移动,但是检查页面是否命中需要遍历链表,时间复杂度也为O(n)
双向链表 + 哈希表
双向链表维护最近使用的和很久没有使用页面的先后顺序,哈希表用于定位页面在双向链表中的位置,方便查找。用双向链表是因为删除链表节点时,需要知道该节点的前驱节点。
双向链表两边特意加上虚拟头节点和虚拟伪节点,以使得链表两端元素的插入和删除操作与链表中间的元素保持一致。
基于Java中LinkedHashMap实现
在Java中,我们可以使用LinkedHashMap来实现LRU算法。LinkedHashMap是Java集合框架中的一个具体实现,它继承自HashMap,但是又以双向链表的形式维护元素的顺序。我们可以设置LinkedHashMap的访问顺序模式(accessOrder)为true,这样每次访问一个元素时,它会被放到最后。当缓存空间不足时,我们只需要删除链表头部的元素即可。
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true);//设置访问顺序为按访问时间排序 this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity;// 当元素数量超过容量时移除最久未被访问的元素 } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "A"); cache.put(2, "B"); cache.put(3, "C"); System.out.println(cache); // 输出:{1=A, 2=B, 3=C} cache.get(2); // 访问key为2的元素 System.out.println(cache); // 输出:{1=A, 3=C, 2=B} cache.put(4, "D"); // 添加新的元素,触发删除最久未被访问的元素 System.out.println(cache); // 输出:{3=C, 2=B, 4=D} } }
我们创建了一个LRUCache类,继承自LinkedHashMap。在构造函数中,我们指定了缓存的容量。removeEldestEntry方法被重写,用于判断是否需要删除最久未被访问的元素。如果缓存超过容量,我们返回true,表示需要删除最老的元素。在main方法中,我们创建了一个容量为3的LRU缓存,进行了一些操作来展示LRU算法的工作机制。
基于自定义数据结构实现(HashMap+双向链表)
import java.util.HashMap; import java.util.Map; /** * @Desc 采用LRU置换算法的缓存 */ public class LRUCache<K, V> { // 静态内部类,双向链表中的节点类,key理解为页面号,val理解为页面内容 static class Entry<K, V> { public Entry<K, V> prev; public Entry<K, V> next; public K key; public V val; public Entry() {} public Entry(K key, V val) { this.key = key; this.val = val; } } private Entry<K, V> head, tail; // 虚拟头节点和虚拟尾节点 private final int capacity; // 缓存容量 private int size; // 缓存占用量 Map<K, Entry<K, V>> cache; // 哈希表,记录双向列表节点的地址值 public LRUCache(int capacity) { this.capacity = capacity; initCache(); } // 初始化LRU缓存 private void initCache() { head = new Entry<>(); tail = new Entry<>(); head.next = tail; tail.prev = head; size = 0; cache = new HashMap<>(this.capacity); } private V get(K key) { Entry<K, V> entry = cache.get(key); if(entry != null) { moveToTail(entry); return entry.val; } else { return null; } } private void put(K key, V val) { Entry<K, V> entry = cache.get(key); if(entry != null) { // 缓存命中 entry.val = val; moveToTail(entry); } else { // 缓存未命中 if(size == capacity) { // 缓存已满,删除链表头部节点 Entry<K, V> h = head.next; deleteEntry(h); cache.remove(h.key); size--; } // 添加新页面到链表尾部 Entry<K, V> newEntry = new Entry<>(key, val); addToTail(newEntry); cache.put(key, newEntry); size++; } } private void moveToTail(Entry<K, V> entry) { deleteEntry(entry); addToTail(entry); } private void addToTail(Entry<K, V> entry) { if(entry != null) { entry.next = tail; entry.prev = tail.prev; tail.prev.next = entry; tail.prev = entry; } } private void deleteEntry(Entry<K, V> entry) { if(entry != null) { entry.prev.next = entry.next; entry.next.prev = entry.prev; } } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(2); cache.put(1,"可口可乐"); cache.put(2,"雪碧"); System.out.println("页面1的内容:" + cache.get(1)); cache.put(3,"果粒橙"); // 此时缓存已满,且页面2最久未被使用(因为cache.get(1)访问了页面1),页面2被置换成页面3 System.out.println("页面2的内容:" + cache.get(2)); // 页面2已被换出,访问不到 } }
运行结果:
页面1的内容:可口可乐 页面2的内容:null