目录
LRU(Least Recently Used)算法
数据结构
LRU 算法主要依赖两个数据结构:
- 双向链表(Doubly Linked List):用于维护缓存的顺序,链表头部(head)是最近使用的节点,链表尾部(tail)是最久未使用的节点。
- 哈希表(HashMap):用于快速查找缓存中的数据项,以保证 O(1) 时间复杂度的访问和更新。
工作原理
LRU 算法的基本思想是,当缓存已满时,淘汰最久未使用的数据项。每次访问缓存时,都会将访问的数据项移动到链表头部,这样链表尾部的数据项就是最久未使用的,可以被优先淘汰。
具体实现步骤
- 初始化:创建一个固定大小的双向链表和一个哈希表。
- 访问数据:
- 命中缓存:如果数据在缓存中,将该数据项移动到链表头部。
- 未命中缓存:如果数据不在缓存中,创建一个新的节点并插入到链表头部。如果缓存已满,移除链表尾部的节点(最久未使用的节点),并从哈希表中删除对应的键。
- 更新数据:更新数据时,将更新后的节点移动到链表头部。
- 淘汰数据:当缓存已满时,移除链表尾部的节点,并从哈希表中删除对应的键。
性能
- 时间复杂度:
- 插入、删除、查找操作的时间复杂度均为 O(1)。
- 空间复杂度:
- 需要额外的空间来维护双向链表和哈希表,其空间复杂度为 O(n)。
应用场景
LRU 算法广泛应用于以下场景:
- 操作系统的页面置换算法
- 数据库缓冲池
- 浏览器缓存
- CDN 缓存
实现代码
以下是用 Java 实现 LRU 缓存的代码:
import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V> {
private final int capacity;
private final Map<K, Node<K, V>> cache;
private final DoublyLinkedList<K, V> list;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>(capacity);
this.list = new DoublyLinkedList<>();
}
public V get(K key) {
Node<K, V> node = cache.get(key);
if (node == null) {
return null; // Cache miss
}
list.moveToHead(node);
return node.value;
}
public void put(K key, V value) {
Node<K, V> node = cache.get(key);
if (node == null) {
if (cache.size() == capacity) {
Node<K, V> tail = list.removeTail();
cache.remove(tail.key);
}
node = new Node<>(key, value);
cache.put(key, node);
list.addToHead(node);
} else {
node.value = value;
list.moveToHead(node);
}
}
private static class DoublyLinkedList<K, V> {
private final Node<K, V> head;
private final Node<K, V> tail;
public DoublyLinkedList() {
head = new Node<>(null, null);
tail = new Node<>(null, null);
head.next = tail;
tail.prev = head;
}
public void addToHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
public void moveToHead(Node<K, V> node) {
removeNode(node);
addToHead(node);
}
public Node<K, V> removeTail() {
Node<K, V> res = tail.prev;
removeNode(res);
return res;
}
private void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
private static class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}
代码解析
- 初始化:构造函数中初始化了缓存容量、哈希表和双向链表。
- 获取数据(get 方法):
- 通过哈希表查找数据,如果存在,将该节点移动到链表头部,并返回值。
- 如果数据不存在,返回 null。
- 插入数据(put 方法):
- 通过哈希表查找数据,如果存在,更新值并将该节点移动到链表头部。
- 如果数据不存在,先判断缓存是否已满,已满则移除尾部节点,再插入新节点到链表头部,并更新哈希表。
总结
LRU 算法通过双向链表和哈希表的结合,实现了高效的缓存访问和更新。其在操作系统、数据库、浏览器等场景中广泛应用,是一种经典且有效的缓存淘汰策略。
FIFO(First In, First Out)算法
数据结构
FIFO 算法主要依赖以下数据结构:
- 队列(Queue):一个先进先出的数据结构,用于维护缓存的顺序。队列头部(front)是最先进入缓存的数据,队列尾部(rear)是最近进入缓存的数据。
工作原理
FIFO 算法的基本思想是,当缓存已满时,淘汰最先进入缓存的数据项,即队列头部的数据项。
具体实现步骤
- 初始化:创建一个固定大小的队列。
- 访问数据:
- 命中缓存:如果数据在缓存中,直接返回数据。
- 未命中缓存:如果数据不在缓存中,插入数据到队列尾部。如果缓存已满,移除队列头部的数据项,再插入新数据。
- 更新数据:更新数据时,将新数据插入到队列尾部,并删除队列头部的老数据(如果缓存已满)。
- 淘汰数据:当缓存已满时,移除队列头部的节点。
性能
- 时间复杂度:
- 插入、删除、查找操作的时间复杂度均为 O(1)。
- 空间复杂度:
- 需要额外的空间来维护队列,其空间复杂度为 O(n)。
应用场景
FIFO 算法适用于以下场景:
- 数据流处理:如网络数据包的缓冲。
- 文件系统缓存:如磁盘块的缓存。
- 打印队列:最先打印的任务最先执行。
实现代码
以下是用 Java 实现 FIFO 缓存的代码:
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
public class FIFOCache<K, V> {
private final int capacity;
private final Map<K, V> cache;
private final Queue<K> queue;
public FIFOCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>(capacity);
this.queue = new LinkedList<>();
}
public V get(K key) {
return cache.get(key);
}
public void put(K key, V value) {
if (cache.size() == capacity) {
K oldestKey = queue.poll();
cache.remove(oldestKey);
}
cache.put(key, value);
queue.offer(key);
}
public static void main(String[] args) {
FIFOCache<Integer, String> cache = new FIFOCache<>(3);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
System.out.println(cache.get(1)); // Output: one
cache.put(4, "four");
System.out.println(cache.get(1)); // Output: null, since it was evicted
System.out.println(cache.get(2)); // Output: two
}
}
代码解析
- 初始化:构造函数中初始化了缓存容量、哈希表和队列。
- 获取数据(get 方法):
- 通过哈希表查找数据,如果存在,返回数据。
- 如果数据不存在,返回 null。
- 插入数据(put 方法):
- 如果缓存已满,移除队列头部的最旧数据,并从哈希表中删除对应的键。
- 将新数据插入到哈希表和队列尾部。
总结
FIFO 算法通过队列实现了先进先出的缓存策略,简单且高效。在需要保证数据处理顺序的应用场景中,FIFO 算法非常适用。相比于 LRU 算法,FIFO 算法实现更加简单,但可能会出现“Belady's Anomaly”,即增加缓存大小反而增加缺页率的现象。
SOFT(软引用)缓存回收策略
工作原理
软引用缓存回收策略利用 Java 的软引用机制。当系统内存不足时,垃圾回收器会回收软引用指向的对象。软引用通常用于缓存实现,因为它允许在内存充足时保留缓存,在内存不足时释放缓存以防止内存溢出。
具体实现步骤
- 初始化:创建一个软引用缓存,使用
java.lang.ref.SoftReference
包装缓存对象。 - 访问数据:
- 命中缓存:如果数据在缓存中且未被回收,返回数据。
- 未命中缓存:如果数据不在缓存中或已被回收,从数据源加载数据并放入缓存。
- 更新数据:更新数据时,使用软引用重新包装新的缓存数据。
- 回收数据:当系统内存不足时,垃圾回收器自动回收软引用指向的对象。
性能
- 时间复杂度:
- 插入、删除、查找操作的时间复杂度通常为 O(1)。
- 空间复杂度:
- 需要额外的空间来维护软引用,其空间复杂度为 O(n)。
- 内存使用:
- 内存充足时,软引用缓存尽可能保留更多数据。
- 内存不足时,垃圾回收器会回收软引用数据,防止内存溢出。
应用场景
软引用缓存适用于以下场景:
- 缓存实现:需要缓存数据但不希望缓存占用过多内存导致内存溢出。
- 内存敏感应用:需要根据内存使用情况动态调整缓存占用。
实现代码
以下是用 Java 实现软引用缓存的代码:
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
public class SoftCache<K, V> {
private final Map<K, SoftReference<V>> cache;
public SoftCache() {
this.cache = new HashMap<>();
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
return (ref != null) ? ref.get() : null;
}
public void put(K key, V value) {
cache.put(key, new SoftReference<>(value));
}
public void remove(K key) {
cache.remove(key);
}
public static void main(String[] args) {
SoftCache<Integer, String> cache = new SoftCache<>();
cache.put(1, "one");
cache.put(2, "two");
System.out.println(cache.get(1)); // Output: one
System.gc(); // Suggest garbage collection
System.out.println(cache.get(1)); // Output: one (if not collected)
}
}
代码解析
- 初始化:构造函数中初始化了软引用缓存,使用
HashMap
存储软引用对象。 - 获取数据(get 方法):
- 通过键查找软引用对象,如果软引用未被回收,返回缓存数据。
- 如果软引用被回收,返回 null。
- 插入数据(put 方法):
- 使用软引用包装新数据,并存储到
HashMap
中。
- 使用软引用包装新数据,并存储到
- 删除数据(remove 方法):
- 从
HashMap
中移除软引用对象。
- 从
总结
软引用缓存回收策略利用 Java 软引用机制,在内存充足时保留缓存数据,在内存不足时自动回收缓存数据,防止内存溢出。软引用缓存适用于内存敏感的缓存实现,保证缓存数据在内存紧张时能够被及时回收。相比于强引用缓存,软引用缓存更能动态适应内存变化。
WEAK(弱引用)缓存回收策略
工作原理
弱引用缓存回收策略利用 Java 的弱引用机制。弱引用在垃圾回收时比软引用更容易被回收。只要弱引用指向的对象没有强引用,垃圾回收器就会立即回收该对象。弱引用常用于缓存实现,因为它允许缓存对象在不被强引用时随时被回收。
具体实现步骤
- 初始化:创建一个弱引用缓存,使用
java.lang.ref.WeakReference
包装缓存对象。 - 访问数据:
- 命中缓存:如果数据在缓存中且未被回收,返回数据。
- 未命中缓存:如果数据不在缓存中或已被回收,从数据源加载数据并放入缓存。
- 更新数据:更新数据时,使用弱引用重新包装新的缓存数据。
- 回收数据:当系统内存不足或垃圾回收器运行时,弱引用指向的对象会被立即回收。
性能
- 时间复杂度:
- 插入、删除、查找操作的时间复杂度通常为 O(1)。
- 空间复杂度:
- 需要额外的空间来维护弱引用,其空间复杂度为 O(n)。
- 内存使用:
- 弱引用缓存在任何时候都可以被垃圾回收器回收,内存占用较少。
应用场景
弱引用缓存适用于以下场景:
- 缓存实现:需要缓存数据但希望缓存占用的内存尽可能少。
- 短命数据缓存:缓存的数据不需要长时间保留,可以随时被回收。
实现代码
以下是用 Java 实现弱引用缓存的代码:
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class WeakCache<K, V> {
private final Map<K, WeakReference<V>> cache;
public WeakCache() {
this.cache = new HashMap<>();
}
public V get(K key) {
WeakReference<V> ref = cache.get(key);
return (ref != null) ? ref.get() : null;
}
public void put(K key, V value) {
cache.put(key, new WeakReference<>(value));
}
public void remove(K key) {
cache.remove(key);
}
public static void main(String[] args) {
WeakCache<Integer, String> cache = new WeakCache<>();
cache.put(1, "one");
cache.put(2, "two");
System.out.println(cache.get(1)); // Output: one
System.gc(); // Suggest garbage collection
System.out.println(cache.get(1)); // Output: null (if collected)
}
}
代码解析
- 初始化:构造函数中初始化了弱引用缓存,使用
HashMap
存储弱引用对象。 - 获取数据(get 方法):
- 通过键查找弱引用对象,如果弱引用未被回收,返回缓存数据。
- 如果弱引用被回收,返回 null。
- 插入数据(put 方法):
- 使用弱引用包装新数据,并存储到
HashMap
中。
- 使用弱引用包装新数据,并存储到
- 删除数据(remove 方法):
- 从
HashMap
中移除弱引用对象。
- 从
总结
弱引用缓存回收策略利用 Java 弱引用机制,在不需要时随时回收缓存数据,减少内存占用。弱引用缓存适用于需要短暂缓存的场景,确保缓存数据在内存紧张时能够被及时回收。相比于软引用缓存,弱引用缓存更容易被回收,适合缓存频繁更新和访问的数据。
总结
-
LRU:
- 优点:适用于频繁访问和更新的数据缓存,能够有效利用最近访问数据的特性。
- 缺点:需要维护链表结构,可能带来额外的内存和时间开销。
-
FIFO:
- 优点:实现简单,适用于有明显先进先出顺序的数据缓存。
- 缺点:无法根据访问频率调整缓存策略,可能导致不常使用的数据占用缓存。
-
SOFT:
- 优点:利用软引用机制,在内存不足时自动回收缓存对象,适用于希望缓存对象在内存不足时被回收的场景。
- 缺点:回收时机不确定,可能导致缓存命中率降低。
-
WEAK:
- 优点:利用弱引用机制,在不被强引用时随时回收缓存对象,适用于短命数据缓存。
- 缺点:回收时机过于频繁,可能导致缓存命中率较低。
每种缓存回收策略都有其优缺点,具体选择哪种策略需要根据应用场景、数据访问模式和性能要求来确定。在实际应用中,可能需要结合多种策略以达到最佳效果。