一、面试题原型
题目:
设计并实现一个线程安全的LRU缓存(Least Recently Used Cache),要求:
- 支持
get(key)
和put(key, value)
操作,时间复杂度均为O(1) - 容量为N时,插入新元素需淘汰最近最少使用的元素
- 使用C++17标准实现,需考虑高并发场景下的线程安全性
二、考察点分析
1. 数据结构设计能力
- 需结合
哈希表
(快速定位)和双向链表
(维护访问顺序)实现O(1)复杂度 - 需理解STL容器特性(如
std::list
的迭代器失效规则)
2. 线程安全实现
- 锁粒度控制(全局锁 vs 分段锁)
- 原子操作与内存序选择(
std::memory_order
) - 避免死锁的编程范式(RAII锁管理)
3. 内存管理
- 节点内存分配策略(对象池 vs 新态分配)
- 异常安全保证(移动语义与
noexcept
)
4. 性能优化
- 缓存淘汰策略的扩展性
- 无锁数据结构的适用场景分析
三、参考解答(C++17实现)
基础版实现(带锁)
#include <unordered_map>
#include <list>
#include <mutex>
template<typename K, typename V>
class LRUCache {
public:
LRUCache(size_t capacity) : capacity_(capacity) {}
V get(const K& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_map.find(key);
if (it == cache_map.end()) return V(); // 返回默认值
// 移动到链表头部
usage_list.splice(usage_list.begin(), usage_list, it->second);
return it->second->second;
}
void put(const K& key, const V& value) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_map.find(key);
if (it != cache_map.end()) {
// 更新值并移动位置
it->second->second = value;
usage_list.splice(usage_list.begin(), usage_list, it->second);
return;
}
// 容量满时淘汰
if (cache_map.size() >= capacity_) {
auto last = usage_list.back();
usage_list.pop_back();
cache_map.erase(last.first);
}
// 插入新元素到头部
usage_list.emplace_front(key, value);
cache_map[key] = usage_list.begin();
}
private:
size_t capacity_;
std::list<std::pair<K, V>> usage_list; // 维护访问顺序
std::unordered_map<K, typename std::list<std::pair<K, V>>::iterator> cache_map;
std::mutex mutex_;
};
高级优化方向
分段锁优化
将哈希表拆分为多个桶(如16个),每个桶独立加锁:
struct Bucket {
std::list<std::pair<K, V>> usage_list;
std::unordered_map<K, typename std::list<...>::iterator> cache_map;
std::mutex bucket_mutex;
};
std::vector<Bucket> buckets;
2.无锁化设计
使用std::atomic
实现无锁链表(C++20支持):
struct Node {
K key;
V value;
std::atomic<Node*> prev;
std::atomic<Node*> next;
};
3.内存池预分配
通过std::pmr::polymorphic_allocator
实现内存池管理:
using Allocator = std::pmr::polymorphic_allocator<std::pair<const K, V>>;
std::pmr::list<std::pair<K, V>> usage_list{Allocator()};
四、高频追问点及应对策略
Q1:为什么使用std::list
而不是std::vector
?
- 考察点:数据结构特性理解
- 回答要点:
std::list
的插入/删除操作为O(1)(已知迭代器位置)std::vector
的中间插入会导致元素移动(O(N)复杂度)- 双向链表结构天然适合维护访问顺序
Q2:如何实现缓存命中率的统计?
- 扩展设计:
struct CacheStats {
size_t hits = 0;
size_t misses = 0;
std::atomic<size_t> concurrent_access{0};
};
- 通过原子操作更新统计信息,避免锁竞争。
Q3:如何处理缓存雪崩问题?
- 解决方案:
- 随机化淘汰时间(在LRU基础上增加随机因子)
- 设置淘汰延迟(淘汰操作异步化)
- 热点数据预加载(结合监控系统)
五、复习指南与资源推荐
核心知识点图谱
资源推荐:
C/C++学习交流君羊 << 点击加入