LRU算法分析与实现
这篇文章我们讨论一下最常见的LRU算法,这个算法对于开发人员来说,是十分重要的,通常被应作操作系统的页面置换算法,缓存系统用该算法作用key的淘汰策略等。
LRU算法的概念及作用
LRU的英文全称叫:Least Recently Used,也就是最近最少使用。
那么什么叫最近最少使用呢?
以缓存为例:比如说我们有一个缓存Cache,只能缓存三个用户(User)对象。
现在我们的Cache中已经被放入了三个用户:
User(张三, 3分钟前被使用过),
User(李四, 5分钟前被使用过),
User(王五, 1分钟被使用过).
现在如果我们往缓存里放入User(赵六,新建数据),而我们的缓存中又只能放三个用户对象,那么如果不淘汰掉一个,我们就没有办法往缓存中放入新的数据。按照LRU的原则,我们应该淘汰掉User(李四,5分钟前被使用过)。
LRU算法最常用的地方除了操作系统虚拟内存的页面置换,还有一个地方就是我们平时开发时涉及到的缓存系统,缓存系统中使用LRU作为key的淘汰算法,可以有效避免因为往缓存中写入的key太多而导致缓存系统内存耗光的问题。
LRU算法的设计思路
下面我们介绍一种比较常规的思路:就是采用Map和双向链表结合的办法来实现LRU算法,通过下图我们可以看看它的具体存储结构:
算法时间复杂度分析:
查找:O(1)
删除:O(1)
添加:O(1)
该结构可以同时满足缓存的增、删、查操作的时间复杂度都为O(1)。因此,目前采用这种思路来实现LRU是比较常见的。
LRU算法的代码实现
前面我们已经介绍了LRU的概念及实现思路,下面我们给出基本上述思路的实现代码:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LRUCache<K,V> {
/**
* map用来存在所有keys及其value
*/
private ConcurrentHashMap<K, Node> map = new ConcurrentHashMap<>();
/**
* 为了保证并发安全,对缓存的读写操作都需要进行加锁
* 为什么读取操作也要加锁呢?那是因为每次访问(读或写)节点,我们都需要更新它的访问时间,
* 确认当前被访问的节点是最近被访问过的
*/
private Lock lock = new ReentrantLock();
/**
* 缓存的最大容量
*/
private int capacity;
/**
* 缓存当前大小
*/
private long size = 0L;
/**
* 双端链表的头节点
*/
private Node head;
/**
* 双端链接的尾节点
*/
private Node tail;
public long getSize() {
return size;
}
public long getCapacity() {
return capacity;
}
public LRUCache(int capacity) {
if(capacity < 2) {
throw new IllegalArgumentException("capacity must be greater than 2");
}
this.capacity = capacity;
}
public void remove(K key) {
try {
lock.lock();
removeKey(key);
} finally {
lock.unlock();
}
}
private void removeKey(K key) {
if(head == null) {
return;
}
Node node = map.get(key);
if(node == null) {
return;
}
if(head == tail && node == head) {
head = tail = null;
} else if(head == node) {
Node temp = head.next;
head.next.prev = null;
head.next = null;
head = temp;
} else if(tail == node) {
Node temp = tail.prev;
tail.prev.next = null;
tail.prev = null;
tail = temp;
} else {
removeNode(node);
}
map.remove(key);
size--;
}
public void put(K key, V value) {
try {
lock.lock();
if(size >= capacity) {
//如果超出容量限制,则需要先淘汰掉队尾
removeTail();
}
enq(key, value);
} finally {
lock.unlock();
}
}
private void enq(K key, V value) {
//将当前key-value包装成新的队头
Node newNode = new Node();
newNode.setKey(key);
newNode.setValue(value);
Node oldNode = map.putIfAbsent(key, newNode);
if(oldNode == null) {//新增节点
if(head == null) {
newNode.setVisitTime(System.currentTimeMillis());
tail = head = newNode;
} else {
setHead(newNode);
}
} else {//更新节点
if(oldNode == head) {
head.setValue(newNode.getValue());
head.setVisitTime(newNode.getVisitTime());
} else if(oldNode == tail) {
Node temp = tail;
setHead(tail);
tail = temp;
} else {
removeNode(oldNode);
setHead(oldNode);
}
}
size++;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
node.next = null;
node.prev = null;
}
private void setHead(Node node) {
//更新当前节点的被访问时间
node.setVisitTime(System.currentTimeMillis());
/**
* 如果访问的是队尾节点,有可能队尾节点会变成队头结点,
* 那么此时,我们应该记录下队尾节点的前驱节点
*/
Node tailPrev = null;
if(node == tail) {
tailPrev = tail.prev;
}
node.next = head;
node.prev = null;
head.prev = node;
head = node;
if(tailPrev != null) {
//除尾节点前驱不为null,则我们要同步更新队尾指针,保证双向链表的完整性
tailPrev.next = tail;
tail.prev = tailPrev;
tail.next = null;
}
}
private void removeTail() {
map.remove(tail.getKey());
Node temp = tail;
tail = tail.prev;
tail.next = null;
temp.prev = null;
size--;
}
public V get(K key) {
Node node = null;
try {
lock.lock();
node = map.get(key);
if(node != null) {
setHead(node);
}
} finally {
lock.unlock();
}
return node == null ? null : node.getValue();
}
class Node {
private Node prev;
private Node next;
private K key;
private V value;
private long visitTime;
public Node getPrev() {
return prev;
}
public void setPrev(Node prev) {
this.prev = prev;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
public long getVisitTime() {
return this.visitTime;
}
public void setVisitTime(final long visitTime) {
this.visitTime = visitTime;
}
public Node() {
}
}
}