操作系统中,内存被占满之后还需调入新的内存页,就需要将别内存页进行调出。而决定内存页如何调出的算法被称为页面调度算法,LRU(Least-Recently-Used,最少最近使用)算法是其中一种。它会将最近使用的页面,进行标记,不至于优先被调出至交换区。内存调页算法被广泛用于本地缓存,本文介绍两种简单的实现方法:1、利用HashMap和双向链表实现2、封装JDK中LinkedHashMap实现
1、使用双向链表、两个HashMap
class Node<V> {
V value;
public Node<V> last;
public Node<V> next;
public Node(V value) {
this.value = value;
}
}
class DoubleNodeList<V> {
private Node<V> head;
private Node<V> tail;
public void addNode(Node newNode) {
if (newNode == null) {
return;
}
if (head == null) {
head = newNode;
tail = newNode;
} else {
tail.next = newNode;
newNode.last = tail;
tail = newNode;
}
}
public void moveToTail(Node node) {
if (tail == null) {
return;
}
if (head == node) {
head = head.next;
head.last = null;
} else {
node.last.next = node.next;
node.next.last = node.last;
}
node.last = tail;
node.next = null;
tail.next = node;
tail = node;
}
public Node<V> removeHead() {
if (head == null) {
return null;
}
Node<V> res = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = res.next;
res.next = null;
head.last = null;
}
return res;
}
}
public class LRU<K, V> {
private java.util.HashMap<K, Node<V>> keyNodeMap;
private java.util.HashMap<Node<V>, K> nodeKeyMap;
private DoubleNodeList<V> list;
private int size;
public LRU(int size) {
keyNodeMap = new java.util.HashMap<>();
nodeKeyMap = new java.util.HashMap<>();
list = new DoubleNodeList<>();
this.size = size;
}
public void put(K key, V val) {
if (keyNodeMap.containsKey(key)) {
Node<V> node = keyNodeMap.get(key);
node.value = val;
keyNodeMap.put(key, node);
nodeKeyMap.put(node, key);
list.moveToTail(node);
} else {
Node<V> newNode = new Node(val);
keyNodeMap.put(key, newNode);
nodeKeyMap.put(newNode, key);
list.addNode(newNode);
if (keyNodeMap.size() == size + 1) {
deleteNode();
}
}
}
public void deleteNode() {
Node<V> delNode = list.removeHead();
Key key = nodeKeyMap.get(delNode);
keyNodeMap.remove(key);
nodeKeyMap.remove(delNode);
}
public V get(K key) {
if (keyNodeMap.containsKey(key)) {
Node<V> node = keyNodeMap.get(key);
list.moveToTail(node);
return node.value;
}
return null;
}
public static void main(String[] args) {
LRU<String, Integer> cache = new LRU<String, Integer>();
cache.put("A", 1);
cache.put("B", 2);
cache.put("C", 3);
cache.get("A");
cache.put("D", 4);
System.out.println("B" + " : " + cache.get("B")); // 可以实现Iterable接口,实现遍历
System.out.println("A" + " : " + cache.get("A"));
System.out.println("C" + " : " + cache.get("C"));
System.out.println("D" + " : " + cache.get("D"));
}
2、封装JDK1.8中LinkedHashMap实现,进行代码重用
public class LRUExtendsLinkedHashMap<K,V> extends java.util.LinkedHashMap<K,V> {
private int maxCapacity;
public LRUExtendsLinkedHashMap() {
}
// LinkedHashMap 有个构造方法带accessOrder参数,这个参数可以在访问元素时,将元素移动到将要删除元素的另一端
public LRUExtendsLinkedHashMap(int max,float loadFactor, boolean accessOrder) {
super(3, loadFactor, accessOrder);
maxCapacity = max;
}
private static final long serialVersionUID = -3892648982214745509L;
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > maxCapacity;
}
public void test() {
LRUExtendsLinkedHashMap<String,Integer> cache = new LRUExtendsLinkedHashMap<String,Integer>(3, 0.75F,true);
cache.put("A", 1);
cache.put("B", 2);
cache.put("C", 3);
cache.get("A");
cache.put("D", 4);
for (java.util.Map.Entry<String, Integer> entry : cache.entrySet()) {
String key = entry.getKey();
Integer val = entry.getValue();
System.out.println(key + " : " + val.intValue());
}
}
public static void main(String[] args) {
new LRUExtendsLinkedHashMap().test();
}
}
LRU缓存的缺点是每次访问元素,需要移动链表中节点以在调出节点都在头部进行,这会占用CPU时间;Linux内核中提出了这样的办法:CPU中使用一个计数器,每一次CPU周期,计数器的值自增,当访问LRU缓存中的元素时,将CPU计数器值赋给链表节点维护的计数器值,每次进行调页时,只需遍历链表,找出节点中计数值最小的节点调出到交换区,就不需要每次都移动链表。