LRU原理
LRU 是 Least Recently Used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大概率将会再次被使用。而最近很少被使用的数据,很大概率下一次不再用到。当缓存容量的满时候,优先淘汰最近很少使用的数据。
假如现在的用户缓存如下:
- 假设我们使用哈希链表来缓存用户信息,目前缓存了四个用户,这四个用户依次按照时间顺序从链表右端插入
- 此时,用户访问用户005,由于哈希表中没有用户005的数据,所以我们从数据库中读取出来,插入到缓存中。这时候,链表中最右端是最新访问的用户005,最左端是最近最少的用户001。
3.接下来,业务访问用户002,哈希表中存在用户002,那么我们会怎么办呢? 我们吧用户002从节点中移除掉,重新插入到最右端。这时候,链表中最右端变成了最新访问到的用户002,最左端仍然是最近最少访问的用户的001。
4.接下来,业务修改用户004的信息,那么我们就把用户004从原来的位置上移动到链表最右侧,并把用户信息的值更新掉。这时候,链表中最右端是最新访问到的用户004,最左端仍然是最近最少访问的用户001。
5.后来业务访问用户006,用户006不在缓存中,需要从数据库中查询出来,然后加入到哈希表中。假设这个时候哈希表的容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户001就会被删除掉,然后再把006插入到最右端。
以上就是LRU算法的基本思路。
LRU 实现一
实现代码如下:
public class Node {
public Node pre;
public Node next;
public String key;
public String value;
public Node(String key ,String value){
this.key = key;
this.value = value;
}
}
public class LRUCache {
private Node head;
private Node end;
//缓存存储上限
private int limit;
private HashMap<String,Node> hashMap;
public LRUCache(int limit){
this.limit = limit;
hashMap=new HashMap<>();
}
public String get(String key){
Node node = hashMap.get(key);
if(node == null){
return null;
}
refreshNode(node);
return node.value;
}
public void put(String key,String value){
Node node = hashMap.get(key);
if(node == null){
// key 不存在,则插入 key - value
if(hashMap.size() >= limit){
String oldKey = removeNode(head);
hashMap.remove(oldKey);
}
node = new Node(key,value);
addNode(node);
hashMap.put(key,node);
}else {
//如果key存在,刷新key- value
node.value = value;
refreshNode(node);
}
}
public void remove(String key){
Node node = hashMap.get(key);
removeNode(node);
hashMap.remove(key);
}
/**
* 刷新被访问的节点位置
* @param node
*/
private void refreshNode(Node node) {
// 如果访问的是尾节点,则无须移动节点
if(node == end){
return;
}
//删除节点
removeNode(node);
//重新插入节点
addNode(node);
}
/**
* 尾部插入节点
* @param node
*/
private void addNode(Node node) {
if(end != null){
end.next = node;
node.pre = end;
node.next=null;
}
end = node;
if(head == null){
head = node;
}
}
/**
* 删除节点
* @param node
*/
private String removeNode(Node node) {
if(node == end){
// 移除尾结点
end = end.pre;
}else if(node == head){
// 移除头节点
head = head.next;
}else{
//移除中间节点
node.pre.next = node.next;
node.next.pre = node.pre;
}
return node.key;
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(5);
lruCache.put("001","张三");
lruCache.put("002","李四");
lruCache.put("003","王五");
lruCache.put("004","tom");
lruCache.put("005","Jack");
lruCache.get("002");
lruCache.put("004","(修改)李四");
lruCache.put("006","andy");
}
}
上面代码不是线程安全的,要想做到线程安全,需要 加上synchronized修饰符。
LRU实现二
由于LinkedHashMap 支持按访问顺序排序双向链表的特性,所以可以基于LinkedHashMap来实现一个LRU缓存,在缓存类中,重写removeEldestEntry方法来定义删除最近最少访问的节点的条件,方法源码如下:
/**
* Returns <tt>true</tt> if this map should remove its eldest entry.
* This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
* inserting a new entry into the map. It provides the implementor
* with the opportunity to remove the eldest entry each time a new one
* is added. This is useful if the map represents a cache: it allows
* the map to reduce memory consumption by deleting stale entries.
*
* <p>Sample use: this override will allow the map to grow up to 100
* entries and then delete the eldest entry each time a new entry is
* added, maintaining a steady state of 100 entries.
* <pre>
* private static final int MAX_ENTRIES = 100;
*
* protected boolean removeEldestEntry(Map.Entry eldest) {
* return size() > MAX_ENTRIES;
* }
* </pre>
*
* <p>This method typically does not modify the map in any way,
* instead allowing the map to modify itself as directed by its
* return value. It <i>is</i> permitted for this method to modify
* the map directly, but if it does so, it <i>must</i> return
* <tt>false</tt> (indicating that the map should not attempt any
* further modification). The effects of returning <tt>true</tt>
* after modifying the map from within this method are unspecified.
*
* <p>This implementation merely returns <tt>false</tt> (so that this
* map acts like a normal map - the eldest element is never removed).
*
* @param eldest The least recently inserted entry in the map, or if
* this is an access-ordered map, the least recently accessed
* entry. This is the entry that will be removed it this
* method returns <tt>true</tt>. If the map was empty prior
* to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
* in this invocation, this will be the entry that was just
* inserted; in other words, if the map contains a single
* entry, the eldest entry is also the newest.
* @return <tt>true</tt> if the eldest entry should be removed
* from the map; <tt>false</tt> if it should be retained.
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
实现一个LRUCache 很容易,几十行代码即可
public class LRUCache2 extends LinkedHashMap {
private int maxElements;
public LRUCache2(int maxElements){
super(maxSize, 0.75F, true);
maxElements = maxElements;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > maxElements;
}
}
LinkedHashMap可以实现LRU算法的缓存基于两点:
1、LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致
2、LinkedList提供了一个boolean值可以让用户指定是否实现LRU
我们看一下LinkedHashMap 自带boolean 型参数的构造方法
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
accessOrder 的含义
- false : 所有Entry 按照插入的顺序排列
- true: 所有的Entry 按照访问的顺序排列
所以要想实现LRU ,accessOrder 必须设置为 true ,如果 有 1 2 3 Entry元素,如果访问1,则1 会移动到末尾去,每次要淘汰的数据的时候,双向队列嘴头的那个数据就是最不常访问的元素,换句话说,双向链表最头的那个数据就是要淘汰的数据。
什么是访问呢? 比如 get 和 put
下面我们从源码上分析get和put 方法
put方法
LinkedHashMap 的 put 方法会直接调用 HashMap 的put 方法,但是方法里会预留方法给LinkedHashMap 访问:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
......
if ((p = tab[i = (n - 1) & hash]) == null)
// LinkedHashMap 会重写 newNode方法
tab[i] = newNode(hash, key, value, null);
else {
......
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// LinkedHashMap 会重写 afterNodeAccess 方法
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
// LinkedHashMap 会重写 afterNodeInsertion 方法
afterNodeInsertion(evict);
return null;
}
我们先不考虑红黑树
- newNode 方法
通过上述两个方法来创建自己的节点,并对before 和 after 进行操作。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
无论是插入顺序还是LRU顺序,新插入的节点都被放入到末尾。
而在HashMap的putVal 方法末尾有这两个方法判断
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// LinkedHashMap 会重写 afterNodeAccess 方法
afterNodeAccess(e);
return oldValue;
}
}
上面代码的意思,当key存在时,直接更新值,afterNodeAccess方法被调用,该方法在HashMap 中为空,在LinkedHashMap中实现,作用是对accessOrder 为 true 情况( LRU 顺序) 下 将该节点调到末尾,因为它被改动了。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
将e节点的前一个节点b与后一个节点a连在一起,将e调到末尾。LRU顺序下,末尾节点代表着最新的节点,意思是要么是新插入的,被更改的,被get访问到的。
if (++size > threshold)
resize();
// LinkedHashMap 会重写 afterNodeInsertion 方法
afterNodeInsertion(evict);
上面的代码的意思是,插入新节点后,对于LinkedHashMap 来说要进行afterNodeInsertion 操作,主要作用判断是否要删除head节点,你可以重写removeEldestEntry 方法,执行自己的逻辑,比如数量超过某个值后插入新值会删除最久未使用的值,即头结点。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
get方法
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
具体实现在HashMap的getNode方法里,可以看到若是LRU顺序,则被访问的节点会被放入到末尾。