1 谈谈页面置换算法
页面置换算法指示了在将新页面载入内存时,如何选择合适的旧页面进行淘汰。
为什么要使用页面置换算法呢?在操作系统中,内存不够时,我们需要对旧内存进行淘汰。
在计算机体系中,硬盘容量大且可靠,内容也可以固化,不过它有一个致命的缺点,那就是访问速度太慢,对用户来说是无法忍受的,故需要把要使用的内容加载进内存中。
内存的性质与硬盘恰恰相反,它容量有限,不可靠(断电后内容会丢失),但它访问速度却很快。速度越快的地方,单位成本就越高,容量就越小,新的内容被载入,旧的需要被淘汰,LRU 就是这样的一种淘汰算法。
2 LRU 原理
LRU,即 Least Recently Used,是一种常见的页面置换算法,它会淘汰掉最近最久未使用的页面。
我们做这样一个假设,当数据在最近一段时间经常被访问时,那么它可能在之后也会被经常访问。对于经常访问的数据,我们为了能够让其快速命中,将其保存在内存之中。而对于不经常访问的数据,在容量超出限制之后,我们需要将其淘汰。
假设内存只能容纳3个页大小,访问页的次序分别是7 0 1 2 0 3 0 4(每个数字代表一个页面),为实现 LRU 算法,我们需要实现一个大小为3的链表进行辅助。
访问每一个页面时,对链表进行如下的操作:
- 访问7:链表大小为1,将7插入链表头部
- 访问0:链表大小为2,将0插入链表头部
- 访问1:链表大小为3,将1插入链表头部
- 访问2:链表大小为3,且2不位于链表之中,故将链表底部的7淘汰,然后将2插入链表头部
- 访问0:链表大小为3,且0位于链表之中,将0转移到链表头部
- 访问3:链表大小为3,且3不位于链表之中,故将链表底部的1淘汰,然后将3插入链表头部
- 访问0:链表大小为3,且0位于链表之中,将0转移到链表头部
- 访问4:链表大小为3,且4不位于链表之中,故将链表底部的2淘汰,然后将4插入链表头部
在链表头部的是最近访问的页面,而链表底部是最远时间访问的页面。如果我们访问的页面不在内存且链表大小已经到达上限,我们需要移除链表底部的页面。
3 选择什么样的数据结构实现 LRU?
一提到设计算法,我们脑子里一般第一反应就会想到数组与链表这两种数据结构,那么它们真的适合用来实现 LRU 吗?
我们知道 LRU 即需要查询效率高,也需要增删效率高,在这里忍不住吐槽一句,太贪心啦,怎么总想着十全十美呢?(不过我喜欢哈哈哈,做事就要追求完美)
在学习数据结构这门课程的时候,我们都知道数组查询速率快,增删速率慢;链表查询速率慢,增删速率快,这两兄弟怎么水火不容啊,就没有一种数据结构即查询速率快,增删速率也快的吗?
其实是有的,我们可以选择链表+哈希表的数据结构,链表增删速率快,然后在链表的基础之上使用哈希表辅助查询,由于哈希表的查询速率可以达到 O(1) 的时间复杂度,这样便可完美解决链表查询速率慢的问题了!
4 LRU 设计思路
我们使用双向链表和哈希表来设计一个 LRU。
首先,我们先建一个 Node 内部类,该类包含一个 key-value 对,且拥有一个前驱节点与一个后继节点,双向链表在 Node 内部类的基础上得以构建。
LRU 会记录最大容量大小与当前容量大小,根据 LRU 的性质,如果容量满了,需要对双向链表的尾部进行淘汰处理。在每次新增或访问数据时,都需要将对应的 Node 转移至双向链表的头部。
LRU 的读操作
哈希表的键用来存储 Node 的 key,且哈希表的值用来指向对应该 key 的 Node 节点,这样,当我们根据 key 进行查询时,可根据哈希表直接得到该 key 的 Node 节点,其时间复杂度为 O(1)。注意,在查询的同时需要将该 Node 节点转移至双向链表头部。
LRU 的写操作
当需要执行插入操作时,首先会去哈希表中查询一下是否存在该节点,存在则在哈希表中更新节点的值,然后将该节点移动至双向链表头部;若不存在,构建新节点且插入双向链表头部,并将其添加入哈希表中,如果此时 LRU 空间不足,则淘汰双向链表的尾部节点,同时在哈希表中对尾部节点进行移除。
5 完整代码
package test;
import java.util.HashMap;
import java.util.Map;
public class LRUCache {
//内部类
private class Node {
String key;
int value;
Node pre;
Node next;
Node(String key, int value) {
this.key = key;
this.value = value;
}
}
//最大容量
private int capacity;
//当前大小
private int count;
//双向链表
private Node head, tail;
//哈希表
private Map<String,Node> map;
public LRUCache(int capacity) {
this.capacity = capacity;
this.count = 0;
head = tail = null;
map = new HashMap();
}
//读操作
public int get(String key) {
Node node = map.get(key);
//不存在返回-1
if(node == null) {
return -1;
}
//存在就将该Node节点移至双向链表头部
moveHead(node);
return node.value;
}
//写操作
public void put(String key, int value) {
Node node = map.get(key);
//该节点不在LRUCache
if(node == null) {
//构建新节点
Node newNode = new Node(key,value);
//将新节点添加至头部
moveNewHead(newNode);
//在哈希表中添加该节点
map.put(key,newNode);
} else {
//直接更新哈希表
node.value = value;
//将该节点移至双向链表头部
moveHead(node);
}
}
//正向遍历
public void findForward() {
System.out.println("正向遍历:当前容量大小为" + count);
Node node = head;
while(node != null) {
System.out.println("key = " + node.key + " value = " + node.value);
node = node.next;
}
}
//反向遍历
public void findBackward() {
System.out.println("反向遍历:当前容量大小为" + count);
Node node = tail;
while(node != null) {
System.out.println("key = " + node.key + " value = " + node.value);
node = node.pre;
}
}
//将节点移至双向链表头部,该节点已存在
private void moveHead(Node node) {
if(head == node) {
return ;
} else if(tail == node) {
tail = tail.pre;
tail.next = null;
node.pre = null;
node.next = head;
head.pre = node;
head = node;
} else {
node.pre.next = node.next;
node.next.pre = node.pre;
node.pre = null;
node.next = head;
head.pre = node;
head = node;
}
}
//将节点移至双向链表头部,该节点不存在
private void moveNewHead(Node newNode) {
count++;
if(head == null) {
head = newNode;
tail = newNode;
} else {
newNode.next = head;
head.pre = newNode;
head = newNode;
}
//容量超出
if(count > capacity) {
//将该节点移出哈希表
String key = tail.key;
map.remove(key);
//修改尾节点
tail = tail.pre;
tail.next = null;
count--;
}
}
}