文章目录
LRU(The Least Recently Used,最近最久未使用算法)
介绍
LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
这个缓存算法将最近使用的条目存放到靠近缓存顶部的位置。当一个新条目被访问时,LRU将它放置到缓存的顶部。当缓存达到极限时,较早之前访问的条目将从缓存底部开始被移除。这里会使用到昂贵的算法,而且它需要记录“年龄位”来精确显示条目是何时被访问的。此外,当一个LRU缓存算法删除某个条目后,“年龄位”将随其他条目发生改变。
Java实现
第一种:使用LinkedHashMap
public class LRUCache {
int capacity;
Map<Integer,Integer> map;
public LRUCache(int capacity){
this.capacity = capacity;
map = new LinkedHashMap<>();
}
public int get(int key){
//如果没有找到
if (!map.containsKey(key)){
return -1;
}
//找到了就刷新数据
Integer value = map.remove(key);
map.put(key,value);
return value;
}
public void put(int key,int value){
if (map.containsKey(key)){
map.remove(key);
map.put(key,value);
return;
}
map.put(key,value);
//超出capacity,删除最久没用的即第一个,或者可以复写removeEldestEntry方法
if (map.size() > capacity){
map.remove(map.entrySet().iterator().next().getKey());
}
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(10);
for (int i = 0; i < 10; i++) {
lruCache.map.put(i,i);
System.out.println(lruCache.map.size());
}
System.out.println(lruCache.map);
lruCache.put(10,200);
System.out.println(lruCache.map);
}
第二种:双链表+hashmap
public class LRUCache {
private int capacity;
private Map<Integer,ListNode>map;
private ListNode head;
private ListNode tail;
public LRUCache2(int capacity){
this.capacity = capacity;
map = new HashMap<>();
head = new ListNode(-1,-1);
tail = new ListNode(-1,-1);
head.next = tail;
tail.pre = head;
}
public int get(int key){
if (!map.containsKey(key)){
return -1;
}
ListNode node = map.get(key);
node.pre.next = node.next;
node.next.pre = node.pre;
return node.val;
}
public void put(int key,int value){
if (get(key)!=-1){
map.get(key).val = value;
return;
}
ListNode node = new ListNode(key,value);
map.put(key,node);
moveToTail(node);
if (map.size() > capacity){
map.remove(head.next.key);
head.next = head.next.next;
head.next.pre = head;
}
}
//把节点移动到尾巴
private void moveToTail(ListNode node) {
node.pre = tail.pre;
tail.pre = node;
node.pre.next = node;
node.next = tail;
}
//定义双向链表节点
private class ListNode{
int key;
int val;
ListNode pre;
ListNode next;
//初始化双向链表
public ListNode(int key,int val){
this.key = key;
this.val = val;
pre = null;
next = null;
}
}
}
LFU(Least Frequently Used ,最近最少使用算法)
介绍
顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
LFU 算法的描述:
设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
- set(key,value):将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉。
- get(key):返回key对应的value值。
算法实现策略:考虑到 LFU 会淘汰访问频率最小的数据,我们需要一种合适的方法按大小顺序维护数据访问的频率。LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素,因此我们很容易想到可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表。
Java代码实现
定义Node节点
/** * 节点 */
public static class Node implements Comparable<Node>{
Object key; //键值
Object value; /** * 访问时间 */
long time; /** * 访问次数 */
int count; public Node(Object key, Object value, long time, int count) { this.key = key; this.value = value; this.time = time; this.count = count;
} public Object getKey() { return key;
} public void setKey(Object key) { this.key = key;
} public Object getValue() { return value;
} public void setValue(Object value) { this.value = value;
} public long getTime() { return time;
} public void setTime(long time) { this.time = time;
} public int getCount() { return count;
} public void setCount(int count) { this.count = count;
}
@Override public int compareTo(Node o) { int compare = Integer.compare(this.count, o.count); //在数目相同的情况下 比较时间
if (compare==0){ return Long.compare(this.time,o.time);
} return compare;
}
}
定义LFU类
public class LFU<K,V> { /** * 总容量 */
private int capacity; /** * 所有的node节点 */
private Map<K, Node> caches; /** * 构造方法
* @param size */
public LFU(int size) { this.capacity = size;
caches = new LinkedHashMap<K,Node>(size);
}
/** * 添加元素
* @param key
* @param value
*/
public void put(K key, V value) {
Node node = caches.get(key); //如果新元素
if (node == null) { //如果超过元素容纳量
if (caches.size() >= capacity) { //移除count计数最小的那个key
K leastKey = removeLeastCount();
caches.remove(leastKey);
} //创建新节点
node = new Node(key,value,System.nanoTime(),1);
caches.put(key, node);
}else { //已经存在的元素覆盖旧值
node.value = value;
node.setCount(node.getCount()+1);
node.setTime(System.nanoTime());
}
sort();
}
/** * 排序 */
private void sort() {
List<Map.Entry<K, Node>> list = new ArrayList<>(caches.entrySet());
Collections.sort(list, new Comparator<Map.Entry<K, Node>>() {
@Override public int compare(Map.Entry<K, Node> o1, Map.Entry<K, Node> o2) { return o2.getValue().compareTo(o1.getValue());
}
});
caches.clear(); for (Map.Entry<K, Node> kNodeEntry : list) {
caches.put(kNodeEntry.getKey(),kNodeEntry.getValue());
}
}
/** * 移除统计数或者时间比较最小的那个
* @return
*/
private K removeLeastCount() {
Collection<Node> values = caches.values();
Node min = Collections.min(values); return (K)min.getKey();
}
/** * 获取元素
* @param key
* @return
*/
public V get(K key){
Node node = caches.get(key); if (node!=null){
node.setCount(node.getCount()+1);
node.setTime(System.nanoTime());
sort(); return (V)node.value;
} return null;
}
public static void main(String[] args) {
LFU<Integer, String> lruCache = new LFU<>(5);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
lruCache.put(4, "D");
lruCache.put(5, "E");
lruCache.put(6, "F");
lruCache.get(2);
lruCache.get(2);
lruCache.get(3);
lruCache.get(6);
//重新put节点3
lruCache.put(3,"C");
final Map<Integer, Node> caches = (Map<Integer, Node>) lruCache.caches;
for (Map.Entry<Integer, Node> nodeEntry : caches.entrySet()) {
System.out.println(nodeEntry.getValue().value);
}
}
}
总结
LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速删除,因为它们的引用频率很低。
一般情况下,LFU效率要优于LRU,且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但LFU需要记录数据的历史访问记录,一旦数据访问模式改变,LFU需要更长时间来适用新的访问模式,即:LFU存在历史数据影响将来数据的“缓存污染”效用。