这里写目录标题
Java 自定义实现 LRU(最近最少使用) 缓存算法
方式一基于LinkedHashMap实现LRU缓存算法
LinkedHashMap
- LinkedHashMap继承自HashMap,内部提供了一个removeEldestEntry方法,该方法正是实现LRU策略的关键所在, 且HashMap内部专门为LinkedHashMap提供了3个专用回调方法,afterNodeAccess、 afterNodeInsertion、afterNodeRemoval,这3个方法的字面意思非常容易理解,就是节点访问后、节点插入后、节点删除后 分别执行的行为。基于以上行为LinkedHashMap就可以实现一个LRUCache的功能了。
- 关于LinkedHashMap的eldest:eldest字面意思为最老的,LinkedHashMap中有个叫做accessOrder的字 段,当accessOrder为true时表示LinkedHashMap内部节点按照访问次数排序,最老的节点也就是访问最少的节点。当 accessOrder为false时表示LinkedHashMap内部节点按照插入顺序排序,最老的节点也就是最早插入的节点,该值默认为 false。
代码实现
class LRUCache extends LinkedHashMap<Integer,Integer>{
private int capacity;
public LRUCache(int capacity) {
//初始化LinkedHashMap
super(capacity,0.75F,true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key,-1);
}
public void put(int key, int value) {
super.put(key,value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
方式二自定义双向链表 + 哈希表实现
实现思路
- 通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
- 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的
- 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置
代码实现
class LRUCache {
//自定义双向链表
class LinkedNode {
int key;
int value;
LinkedNode prev;//前驱结点
LinkedNode next;//后继结点
public LinkedNode() {};
public LinkedNode(int _key, int _value) {key = _key; value = _value;}
}
//哈希表
private Map<Integer, LinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
//头结点和尾结点
private LinkedNode head, tail;
//构造器,初始化容量
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
//使用伪头结点和伪为节点
head = new LinkedNode();
tail = new LinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key){
//从hash表中根据key获取双向链表结点位置达到O(1)的时间复杂度
LinkedNode linkedNode = cache.get(key);
//如果hash表中没有值,那么返回默认值-1
if (linkedNode == null) return -1;
//如果存在,把这个值移动到双向链表的头部
//链表的头部记录了最近使用过的结点,尾部记录了最近不常被使用的结点
moveToHead(linkedNode);
return linkedNode.value;
}
public void put(int key, int value) {
LinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
LinkedNode newNode = new LinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
size++;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
LinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
size--;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
//把节点移动到头部
private void moveToHead(LinkedNode node){
//先删除,后添加
removeNode(node);
addToHead(node);
}
//添加结点到链表头部
private void addToHead(LinkedNode node){
//伪头结点的后继结点就是当前要插入的结点
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
//删除结点
private void removeNode(LinkedNode node){
LinkedNode prev = node.prev;
prev.next = node.next;
node.next.prev = prev;
}
//删除尾部结点
private LinkedNode removeTail(){
//因为使用伪头尾结点的原因,伪尾结点的前驱结点就是实际元素结点的尾部
LinkedNode prev = tail.prev;
removeNode(prev);
return prev;
}
}
方式三借助ReadWriteLock(读写锁)+ 双向链表 + 哈希表实现线程安全的LRU缓存算法
实现思路
- 借助ReadWriteLock(读写锁)+ 双向链表 + 哈希表实现线程安全的LRU缓存算法,当get的时候加上读锁,当put的时候加上写锁
代码实现
class LRUCache {
//读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//自定义双向链表
class LinkedNode {
int key;
int value;
LinkedNode prev;//前驱结点
LinkedNode next;//后继结点
public LinkedNode() {};
public LinkedNode(int _key, int _value) {key = _key; value = _value;}
}
//哈希表
private Map<Integer, LinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
//头结点和尾结点
private LinkedNode head, tail;
//构造器,初始化容量
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
//使用伪头结点和伪为节点
head = new LinkedNode();
tail = new LinkedNode();
head.next = tail;
tail.prev = head;
}
//当进行get时加上读锁
public int get(int key){
//上读锁
readWriteLock.readLock().lock();
try {
//从hash表中根据key获取双向链表结点位置达到O(1)的时间复杂度
LinkedNode linkedNode = cache.get(key);
//如果hash表中没有值,那么返回默认值-1
if (linkedNode == null) return -1;
//如果存在,把这个值移动到双向链表的头部
//链表的头部记录了最近使用过的结点,尾部记录了最近不常被使用的结点
moveToHead(linkedNode);
return linkedNode.value;
}finally {
//释放读锁
readWriteLock.readLock().unlock();
}
}
//当进行put操作时加上写锁
public void put(int key, int value) {
//上写锁
readWriteLock.writeLock().lock();
try {
LinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
LinkedNode newNode = new LinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
LinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}finally {
readWriteLock.writeLock().unlock();
}
}
//把节点移动到头部
private void moveToHead(LinkedNode node){
//先删除,后添加
removeNode(node);
addToHead(node);
}
//添加结点到链表头部
private void addToHead(LinkedNode node){
//伪头结点的后继结点就是当前要插入的结点
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
//删除结点
private void removeNode(LinkedNode node){
LinkedNode prev = node.prev;
prev.next = node.next;
node.next.prev = prev;
}
//删除尾部结点
private LinkedNode removeTail(){
//因为使用伪头尾结点的原因,伪尾结点的前驱结点就是实际元素结点的尾部
LinkedNode prev = tail.prev;
removeNode(prev);
return prev;
}
}
最后由于个人技术水平有限,本文难免有不足的地方,请各位给予指正。