解决 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 来检测内存问题,这对我今后的开发工作非常有帮助。

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

相关推荐
天疆说41 分钟前
04 稳定性与性能调优实录:OOM 崩溃、并发槽位、前缀缓存与 DSpark 调参
linux·运维·缓存
csdn2015_1 小时前
springboot +mybatis 查询myql开启程序级别缓存
spring boot·缓存·mybatis
隔窗听雨眠1 小时前
缓存增强生成CAG:用预加载KV缓存突破RAG实时检索瓶颈
缓存
光影少年2 小时前
react离线缓存、图片缓存方案
开发语言·前端·javascript·react native·react.js·缓存·前端框架
m0_3807438712 小时前
给大模型调用加一层本地缓存,让重复请求直接命中磁盘
缓存
FakeOccupational15 小时前
【电路笔记 STM32】Cortex-M7 内核上的数据缓存(D-Cache)结构+MPU+DMA&Cache+STM32CubeMX配置
笔记·stm32·缓存
l1t19 小时前
DeepSeek总结的在 pg_stat_statements 中诊断高基数工作负载
数据库·缓存·postgresql
2401_894915531 天前
新手落地 Geo 优化:源码下载、依赖安装、数据库初始化完整步骤
运维·服务器·数据库·人工智能·缓存·开源
2401_894915531 天前
Geo 优化源码部署避坑指南:解决访问异常、定位失效、收录卡顿问题
java·服务器·后端·缓存·开源
祈禾1 天前
Redis三大缓存问题与分布式锁
运维·数据库·redis·笔记·分布式·缓存