1、设计并实现一个LRU(最近最少使用)缓存,要求get和put操作时间复杂度O(1),并说明如何支持并发安全?
1. 什么是 LRU ?为什么需要它?
LRU(Least Recently Used)即"最近最少使用"淘汰算法。缓存的容量通常是有限的,当缓存满了,需要决定删掉哪个数据来腾出空间。LRU 的核心思想是:"如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。" 因此,当空间不足时,最久没有被访问的数据会被淘汰。
实现 LRU 缓存,需要保证 get(查询)和 put(写入)的时间复杂度都是 O(1)。
2. 基本LRU缓存实现
核心数据结构
- 双向链表:维护访问顺序,头节点是最近使用的,尾节点是最久未使用的
- 哈希表:提供O(1)的查找时间,存储key到链表节点的映射
java
import java.util.HashMap;
import java.util.Map;
public class LRUCache {
private final int capacity;
private final Map<Integer, Node> cache;
// 双向链表的虚拟头尾节点
private final Node head;
private final Node tail;
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
// 创建虚拟头尾节点
this.head = new Node();
this.tail = new Node();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) {
return -1;
}
// 将节点移到头部(标记为最近使用)
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
Node existingNode = cache.get(key);
if (existingNode != null) {
// 更新现有值并移到头部
existingNode.value = value;
moveToHead(existingNode);
return;
}
// 检查容量
if (cache.size() > capacity) {
// 删除最久未使用的节点(尾节点的前一个)
Node lruNode = tail.prev;
removeNode(lruNode);
cache.remove(lruNode.key);
}
// 添加新节点到头部
Node newNode = new Node(key, value);
addToHead(newNode);
cache.put(key, newNode);
}
// 将节点移动到头部
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
// 在头部添加节点
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 删除节点
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 链表节点类
private static class Node {
int key;
int value;
Node prev;
Node next;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
}
2. 并发安全支持
方案一:使用ConcurrentHashMap
java
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentLRUCache {
private final int capacity;
// 使用ConcurrentHashMap
private final ConcurrentHashMap<Integer, Node> cache;
private final Node head;
private final Node tail;
public ConcurrentLRUCache(int capacity) {
this.capacity = capacity;
this.cache = new ConcurrentHashMap<>();
this.head = new Node();
this.tail = new Node();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) {
return -1;
}
// 在同步块中进行链表操作
synchronized(this) {
moveToHead(node);
return node.value;
}
}
public void put(int key, int value) {
// 先更新值,再调整位置
Node existingNode = cache.get(key);
if (existingNode != null) {
synchronized(this) {
existingNode.value = value;
moveToHead(existingNode);
}
return;
}
synchronized(this) {
if (cache.size() > capacity) {
Node lruNode = tail.prev;
removeNode(lruNode);
cache.remove(lruNode.key);
}
Node newNode = new Node(key, value);
addToHead(newNode);
cache.put(key, newNode);
}
}
// 将节点移动到头部
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
// 在头部添加节点
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 删除节点
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 链表节点类
private static class Node {
int key;
int value;
Node prev;
Node next;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
}
方案二:使用读写锁优化
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class OptimizedLRUCache {
private final int capacity;
private final Map<Integer, Node> cache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Node head;
private final Node tail;
public OptimizedLRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
this.head = new Node();
this.tail = new Node();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
// HashMap在外部访问是线程安全的
Node node = cache.get(key);
if (node == null) {
return -1;
}
lock.writeLock().lock();
try {
moveToHead(node);
return node.value;
} finally {
lock.writeLock().unlock();
}
}
public void put(int key, int value) {
lock.writeLock().lock();
try {
Node existingNode = cache.get(key);
if (existingNode != null) {
existingNode.value = value;
moveToHead(existingNode);
return;
}
if (cache.size() > capacity) {
Node lruNode = tail.prev;
removeNode(lruNode);
cache.remove(lruNode.key);
}
Node newNode = new Node(key, value);
addToHead(newNode);
cache.put(key, newNode);
} finally {
lock.writeLock().unlock();
}
}
// 将节点移动到头部
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
// 在头部添加节点
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 删除节点
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 链表节点类
private static class Node {
int key;
int value;
Node prev;
Node next;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
}
3. 时间复杂度分析
- get操作 :O(1)
- 哈希表查找:O(1)
- 双向链表删除/插入:O(1)
- put操作 :O(1)
- 哈希表查找:O(1)
- 容量检查:O(1)
- 节点删除/插入:O(1)
4. 并发安全要点
关键考虑点
- 原子性:链表操作必须是原子的,防止数据不一致
- 可见性:使用volatile或锁确保状态变化对所有线程可见
- 性能:读写锁比独占锁性能更好,因为get操作频繁且不改变结构
最佳实践
- 分离关注点:将哈希表和链表操作分开处理
- 最小化锁范围:只在必要时加锁,减少竞争
- 使用成熟组件 :利用
ConcurrentHashMap等经过验证的并发容器
LeetCode 146. LRU 缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
- LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存。
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
java
public class LRUCache {
private static class Node {
int key;
int value;
Node prev;
Node next;
public Node() {}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private int size;
private int capacity;
private Node head, tail;
private Map<Integer, Node> cache;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
this.cache = new HashMap<Integer, Node>();
head = new Node();
tail = new Node();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
Node node = cache.get(key);
if (node == null) {
return -1;
}
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
Node node = cache.get(key);
if (node == null) {
Node newNode = new Node(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
Node removedNode = removeTail();
cache.remove(removedNode.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(Node node) {
removeNode(node);
addToHead(node);
}
private Node removeTail() {
Node node = tail.prev;
removeNode(node);
return node;
}
}
【参考文献】
https://zhuanlan.zhihu.com/p/1928834973680509448
https://blog.csdn.net/Aision_tean/article/details/160156945
https://blog.csdn.net/Z2076465172/article/details/159174772