目录
一.LRU缓存
1.概念
LRU(Least Recently Used)缓存是一种缓存淘汰策略,用来在内存有限的情况下有效管理缓存内容。LRU策略会移除最近最少使用的缓存项,从而保留较常访问的数据。它通常使用哈希表和双向链表的组合来实现,以提供O(1)的时间复杂度用于插入和访问。
2.实现原理
-
数据结构:
- 哈希表:快速查找缓存项,提供O(1)的时间复杂度。
- 双向链表:维护缓存项的访问顺序,使得尾部的节点代表最近最少使用的数据。
-
操作:
- 访问:如果访问一个缓存数据,将其移动到链表的头部。
- 插入:新数据插入链表头部。
- 淘汰:当缓存达到容量限制时,移除链表尾部的节点。
3.代码示例
import java.util.HashMap;
import java.util.Map;
class LRUCache<K,V> {
class Node<K,V>{
K key;
V value;
Node<K,V> prev;
Node<K,V> next;
public Node() {}
public Node(K key,V value) {
this.key=key;
this.value=value;
}
}
private Map<K,Node<K,V>> hash=new HashMap<>();
int capacity;
private Node<K,V> head,tail;
public LRUCache(int capacity) {
this.capacity=capacity;
head=new Node();
tail=new Node();
head.next=tail;
tail.prev=head;
}
public V get(K key) {
Node<K,V> node=hash.get(key);
if(node==null) {
return null;
}
moveToHead(node);
return node.value;
}
public void put(K key,V value) {
Node<K,V> node=hash.get(key);
if(node==null) {
Node<K,V> newNode=new Node<K,V>(key,value);
hash.put(key,newNode);
addToHead(newNode);
if(hash.size()>capacity) {
//此时进行删除
Node<K,V> temp=removeTail();
hash.remove(temp.key);
}
} else {
node.value=value;
moveToHead(node);
}
}
public Node<K,V> removeTail() {
Node<K,V> ans=tail.prev;
removeNode(ans);
return ans;
}
public void removeNode(Node<K,V> node) {
node.prev.next=node.next;
node.next.prev=node.prev;
}
public void addToHead(Node<K,V> node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
public void moveToHead(Node<K,V> node) {
removeNode(node);
addToHead(node);
}
}
注:流程就是当我们插入到一个数据的时候将其放到双向链表头部位置,这里的head和tail是用来指向双向链表的头结点和尾结点的,俗称为哨兵位。对于已经存在节点,我们无论是查询还是修改值,都会影响这个节点使用的时间,所以每次在使用完之后将其移动到头节点的位置,对于移动节点的时候,这里做的是将其节点删除,在进行插入。所以每次删除节点的时候,tail所指向的节点就是我们要删除的节点。
二.LFU缓存
1.概念
LFU (Least Frequently Used) 缓存是一种缓存淘汰策略,用于在有限的内存中有效管理缓存内容。LFU缓存会移除使用频率最低的缓存项。当缓存容量达到限制时,频率最低的项将被移除。
2.实现原理
-
数据结构:
- 哈希表:用于存储键到节点的映射,提供O(1)时间复杂度的查找。
- 优先队列:用来根据使用频率排序节点,频率最低的节点在队列顶部,提供O(log N)的插入和删除操作。
-
操作:
- 访问:每次访问时增加项的频率,并重新调整优先队列的顺序。
- 插入:如果新数据插入并且达到容量上限,将移除优先队列中最低频率的节点。
3.代码示例
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
public class LFUCache<K,V> {
class Node<K,V>{
K key;
V value;
int frequency;
public Node() {}
public Node(K key,V value) {
this.key=key;
this.value=value;
this.frequency=1;
}
}
private int capacity;
private Map<K,Node<K,V>> hash=new HashMap<>();
private PriorityQueue<Node<K,V>> queue;
public LFUCache(int capacity) {
this.capacity=capacity;
queue=new PriorityQueue<>((node1,node2)->node1.frequency-node2.frequency);
}
public void put(K key,V value) {
if(capacity==0) {
return;
}
Node<K,V> node=hash.get(key);
if(node==null) {
Node<K,V> newNode=new Node<>(key,value);
hash.put(key,newNode);
if(hash.size()>capacity) {
Node<K,V> temp = queue.poll();
hash.remove(temp.key);
}
queue.offer(newNode);
} else {
node.value=value;
node.frequency++;
queue.remove(node);
queue.offer(node);
}
}
public V get(K key) {
Node<K,V> node=hash.get(key);
if(node==null) {
return null;
}
node.frequency++;
queue.remove(node);
queue.offer(node);
return node.value;
}
}
注:这里的频率表示有点简单。在插入节点的时候,先将节点插入到hash中,然后再进行判断缓存大小是否大于容量,大于容量再将堆顶的元素删除。再元素插入到队列中。这时候发现一个问题,我们初始插入的节点频率为1,如果队列中频率都新节点大,那不是要删除新节点吗?
但是我们一想,如果新插入的节点就直接删除的话,那插入还有什么意思。
测试代码
public static void main(String[] args) {
LFUCache cache = new LFUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 输出 1
cache.put(3, 3); // 淘汰频率最低的键 2
System.out.println(cache.get(2)); // 输出 null
System.out.println(cache.get(3)); // 输出 3
cache.put(4, 4); // 淘汰频率最低的键 1
System.out.println(cache.get(1)); // 输出 null
System.out.println(cache.get(3)); // 输出 3
System.out.println(cache.get(4)); // 输出 null
}
在调试的时候断点直接打在cache.put(4,4)这一行,队列里的元素频率都比4这个节点频率大。 但是我们肯定不想将4直接删除,所以要进行优化缓存策略。
4.优化代码
import java.util.*;
public class LFUCacheOptimized {
class Node {
int key, value, frequency;
Node prev, next;
public Node(int key, int value) {
this.key = key;
this.value = value;
this.frequency = 1;
}
}
class FrequencyList {
Node head, tail;
public FrequencyList() {
head = new Node(-1, -1);
tail = new Node(-1, -1);
head.next = tail;
tail.prev = head;
}
public void insert(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
public void remove(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
public Node removeLast() {
if (tail.prev == head) return null;
Node node = tail.prev;
remove(node);
return node;
}
}
private final int capacity;
private Map<Integer, Node> cache;
private Map<Integer, FrequencyList> frequencyMap;
private int minFrequency;
public LFUCacheOptimized(int capacity) {
this.capacity = capacity;
cache = new HashMap<>();
frequencyMap = new HashMap<>();
this.minFrequency = 0;
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) return -1;
updateFrequency(node);
return node.value;
}
public void put(int key, int value) {
if (capacity == 0) return;
Node node = cache.get(key);
if (node == null) {
if (cache.size() == capacity) {
FrequencyList list = frequencyMap.get(minFrequency);
Node removedNode = list.removeLast();
cache.remove(removedNode.key);
}
node = new Node(key, value);
cache.put(key, node);
minFrequency = 1;
frequencyMap.computeIfAbsent(1, k -> new FrequencyList()).insert(node);
} else {
node.value = value;
updateFrequency(node);
}
}
private void updateFrequency(Node node) {
int freq = node.frequency;
FrequencyList list = frequencyMap.get(freq);
list.remove(node);
if (freq == minFrequency && list.head.next == list.tail) {
minFrequency++;
}
node.frequency++;
frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList()).insert(node);
}
public static void main(String[] args) {
LFUCacheOptimized cache = new LFUCacheOptimized(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1)); // 输出 1
cache.put(3, 3); // 淘汰频率最低的键 2
System.out.println(cache.get(2)); // 输出 -1
System.out.println(cache.get(3)); // 输出 3
cache.put(4, 4); // 淘汰频率最低的键 1
System.out.println(cache.get(1)); // 输出 -1
System.out.println(cache.get(3)); // 输出 3
System.out.println(cache.get(4)); // 输出 4
}
}
解析:
frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList()).insert(node);
当使用
frequencyMap.computeIfAbsent(node.frequency, k -> new FrequencyList())
时,如果node.frequency
已经存在于frequencyMap
中,computeIfAbsent
方法将直接返回与node.frequency
关联的FrequencyList
实例,而不是执行提供的映射函数k -> new FrequencyList()
。在这种情况下,频率存在,即Map不会为该频率创建新的
FrequencyList
对象,直接在现有的FrequencyList
中插入节点。
注:如果涉及多线程的场景下,使用ConcurrentHashMap更为妥当。
三.FIFO缓存
1.概念
FIFO(First-In-First-Out)缓存是一种简单但有效的缓存淘汰策略,当缓存达到容量时淘汰最早插入的缓存项。
2.实现原理
- 数据结构:
- LinkedHashMap:保持插入顺序并在达到容量上限时自动删除最早的缓存项。
- 操作:
- 插入: 新数据插入时按顺序放置。
- 淘汰: 当容量达到限制时,自动移除链表中的最早元素。
3.代码示例
import java.util.LinkedHashMap;
import java.util.Map;
class FIFOCache<K, V> {
private final int capacity;
private final LinkedHashMap<K, V> cache;
public FIFOCache(int capacity) {
this.capacity = capacity;
// LinkedHashMap构造方法的第三个参数为false,表示根据插入顺序排序
this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, false) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当缓存大小超过设置的容量时,移除最早插入的元素
return size() > FIFOCache.this.capacity;
}
};
}
// 从缓存获取数据
public V get(K key) {
return cache.get(key);
}
// 向缓存插入数据
public void put(K key, V value) {
cache.put(key, value);
}
// 测试FIFO缓存功能
public static void main(String[] args) {
FIFOCache<String, Integer> fifoCache = new FIFOCache<>(2);
fifoCache.put("A", 1);
fifoCache.put("B", 2);
System.out.println(fifoCache.get("A")); // 输出 1
fifoCache.put("C", 3); // 淘汰最早插入的键 A
System.out.println(fifoCache.get("A")); // 输出 null,A 已被淘汰
System.out.println(fifoCache.get("B")); // 输出 2,B 仍然存在
fifoCache.put("D", 4); // 淘汰最早插入的键 B
System.out.println(fifoCache.get("B")); // 输出 null,B 已被淘汰
System.out.println(fifoCache.get("C")); // 输出 3,C 仍然存在
System.out.println(fifoCache.get("D")); // 输出 4,D 已插入
}
}
LinkedHashMap 构造函数
复制
new LinkedHashMap<K, V>(capacity, 0.75f, false)
- 参数解析:
capacity
:表示构造LinkedHashMap
的初始容量,实际容量可能会根据负载因子和存入的元素动态增大。0.75f
:这是装载因子(load factor),用于控制LinkedHashMap
何时进行扩容。在目前的应用中,不会直接影响缓存策略。false
:指示LinkedHashMap
根据插入顺序存储条目,而不是访问顺序。传入false
确保访问顺序不更改原始的插入顺序,从而实现FIFO逻辑。
内部类和方法重写
复制
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > FIFOCache.this.capacity;
}
- 重写方法
removeEldestEntry
:- 在
LinkedHashMap
中,removeEldestEntry
是被用于判断在每次插入新条目时,是否需要移除最老的条目的方法。 - 条件:
size() > FIFOCache.this.capacity
,检查当前缓存大小是否超过指定容量。 - 如果缓存大小超过设定容量,将移除最早插入的元素(最终以FIFO顺序删除)。
- 在
工作机制
- 插入条目:每当一个新条目插入
LinkedHashMap
时,系统将调用removeEldestEntry
方法来决定是否移除旧条目。 - FIFO策略实现:因为
LinkedHashMap
被配置为按插入顺序存储,并在超出容量时移除最老的条目,因此实现了FIFO策略。
总结常见缓存策略的实现
-
LRU(Least Recently Used)缓存:
- 使用双向链表和哈希表实现。
- 淘汰最近最少使用的数据。
-
LFU(Least Frequently Used)缓存:
- 使用优先队列和哈希表实现。
- 淘汰使用频率最低的数据。
-
FIFO(First-In-First-Out)缓存:
- 使用LinkedHashMap的特殊配置实现。
- 淘汰最早插入的数据。