解决 LRU 缓存中的“堆使用后释放”问题

解决 LRU 缓存中的"堆使用后释放"问题

力扣:https://leetcode.cn/problems/lru-cache/?envType=study-plan-v2\&envId=top-100-liked

牛客:https://www.nowcoder.com/practice/5dfded165916435d9defb053c63f1e84?tpId=295\&tqId=2427094\&sourceUrl=%2Fexam%2Foj

一、问题背景

最近在实现一个 LRU(Least Recently Used)缓存时,遇到了一个棘手的问题。LRU 缓存是一种常见的缓存淘汰策略,用于在缓存容量满时,淘汰最长时间未被使用的数据。我的实现使用了 listunordered_map,但在测试时,AddressSanitizer 报告了一个"堆使用后释放"(heap-use-after-free)的错误。这个问题让我困扰了很久,但最终我找到了解决方法,并从中学习到了很多。奇怪的是,同样的代码,牛客测试可以通过,力扣不能通过。

二、问题描述

LRU 缓存的实现中,list 用于维护访问顺序,unordered_map 用于快速查找。list 存储键值对 (key, value)unordered_map 存储键到 list 中节点的迭代器。这样可以快速找到某个键对应的节点,并将其移动到 list 的头部或从尾部移除。

然而,在测试时,AddressSanitizer 报告了一个错误:

复制代码
ERROR: AddressSanitizer: heap-use-after-free on address 0x503000002304 at pc 0x555c98686c24 bp 0x7ffeacde9f10 sp 0x7ffeacde9f08
READ of size 4 at 0x503000002304 thread T0

这个错误表明代码试图访问一个已经被释放的内存地址。问题的根本原因是 unordered_map 中的迭代器在某些情况下失效了。

三、问题分析

(一)get 方法中的问题

get 方法中,代码试图通过 unordered_map 找到某个键对应的节点,并将其移动到 list 的头部:

cpp 复制代码
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));

问题在于,datalists.erase(mp[key]) 会移除 list 中的节点,导致 mp[key] 指向的迭代器失效。如果后续代码试图通过 mp[key] 访问节点,就会导致访问已释放内存的错误。

(二)put 方法中的问题

put 方法中,当缓存已满时,会移除 list 的尾部节点:

cpp 复制代码
mp.erase(datalists.back().first);
datalists.pop_back();

这里,datalists.pop_back() 会释放尾部节点的内存。如果在此之前有某个键的迭代器指向了这个尾部节点,那么这个迭代器就会失效。

四、解决方法

(一)修复 get 方法

get 方法中,移除节点后,需要更新 unordered_map 中的迭代器,确保它指向新的节点:

cpp 复制代码
int value = mp[key]->second;
datalists.erase(mp[key]);
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();

通过 mp[key] = datalists.begin();,将迭代器更新为指向新插入的节点。

(二)修复 put 方法

put 方法中,移除尾部节点后,需要确保 unordered_map 中没有指向已释放节点的迭代器:

cpp 复制代码
if (mp.find(key) != mp.end()) {
    datalists.erase(mp[key]);
} else if (datalists.size() == cap) {
    mp.erase(datalists.back().first);
    datalists.pop_back();
}
datalists.push_front(make_pair(key, value));
mp[key] = datalists.begin();

通过 mp[key] = datalists.begin();,将迭代器更新为指向新插入的节点。

五、完整的修复代码

以下是修复后的完整代码:

cpp 复制代码
class LRUCache {
public:
    int cap;
    list<pair<int, int>> datalists;
    unordered_map<int, list<pair<int, int>>::iterator> mp;

    LRUCache(int capacity) {
        cap = capacity;
    }

    int get(int key) {
        if (mp.find(key) != mp.end()) {
            int value = mp[key]->second;
            datalists.erase(mp[key]);
            datalists.push_front(make_pair(key, value));
            mp[key] = datalists.begin();
            return value;
        } else {
            return -1;
        }
    }

    void put(int key, int value) {
        if (mp.find(key) != mp.end()) {
            datalists.erase(mp[key]);
        } else if (datalists.size() == cap) {
            mp.erase(datalists.back().first);
            datalists.pop_back();
        }
        datalists.push_front(make_pair(key, value));
        mp[key] = datalists.begin();
    }
};

六、测试用例

为了验证修复后的代码是否正确,我运行了以下测试用例:

cpp 复制代码
LRUCache cache(2);
cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回 1
cache.put(3, 3);    // 该操作会使得 key=2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得 key=1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回 3
cache.get(4);       // 返回 4

运行结果表明,修复后的代码能够正确处理各种情况,没有再出现"堆使用后释放"的错误。

七、总结

通过这次经历,我深刻认识到在使用 listunordered_map 时,必须确保迭代器的有效性。在修改 list 的内容后,要及时更新 unordered_map 中的迭代器,避免访问已释放的内存。同时,我也学会了如何使用 AddressSanitizer 来检测内存问题,这对我今后的开发工作非常有帮助。

希望这篇文章能帮助到遇到类似问题的开发者。

相关推荐
有梦想的攻城狮4 小时前
通过Lettuce实现PB3格式对象在Redis中的存储与查询
数据库·redis·缓存·pb3
一个儒雅随和的男子4 小时前
多级缓存解决方案
spring boot·缓存
⑩-5 小时前
Redis(1)
数据库·redis·缓存
ifeng091811 小时前
HarmonyOS资源加载进阶:惰性加载、预加载与缓存机制
深度学习·缓存·harmonyos
大隐隐于野11 小时前
从零开始理解和编写LLM中的KV缓存
java·缓存·llm
milanyangbo16 小时前
从同步耦合到异步解耦:消息中间件如何重塑系统间的通信范式?
java·数据库·后端·缓存·中间件·架构
像风一样自由202018 小时前
Redis与MinIO:两大存储利器的区别与联系
数据库·redis·缓存·minio
无心水18 小时前
【中间件:Redis】4、Redis缓存实战:穿透/击穿/雪崩的5种解决方案(附代码实现)
redis·缓存·中间件·缓存穿透·缓存雪崩·分布式缓存·redis缓存问题
爱吃烤鸡翅的酸菜鱼19 小时前
【Java】基于策略模式 + 工厂模式多设计模式下:重构租房系统核心之城市房源列表缓存与高性能筛选
java·redis·后端·缓存·设计模式·重构·策略模式
milanyangbo19 小时前
从局部性原理到一致性模型:深入剖析缓存设计的核心权衡
开发语言·后端·缓存·架构