LRU缓存设计与实现

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

相关推荐
甲方大人请饶命2 小时前
SSM-基础
java·数据库·spring
谷雨不太卷2 小时前
Linux基础IO
java·开发语言
小新同学^O^2 小时前
简单学习 --> 文件IO
java·学习·文件io
Purple Coder2 小时前
面试-第二篇方法篇
面试·职场和发展
Cosolar2 小时前
AI Agent 的记忆战争:OpenClaw vs Hermes vs QwenPaw vs HiClaw,谁真正"记得住"?
人工智能·后端·面试
吴声子夜歌2 小时前
Java——Arrays
java·算法·排序算法
M ? A2 小时前
VuReact:Vue转React的增量编译利器
前端·vue.js·后端·react.js·面试·开源·vureact
Purple Coder2 小时前
面试-第一篇心态篇
面试·职场和发展
fanzhonghong2 小时前
javaWeb开发之Maven高级
java·开发语言·spring boot·spring cloud·私服