LRU(Least Recently Used)是一种常见的页面置换算法,在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。
1、数据需要保存,可以使用ConcurrentHashMap
2、需要记录数据的先后关系,常见的保存方式可以是数组、链表,但是使用数组则不方便修改数据保存的位置。大部分同学都应该会刷面试题,arrayList 和 LinkedList 的区别,应该都清楚,LinkedList在数据的增删方面具有极大的优势。但是此处我们更适合双向链表,默认拥有头、尾部节点。
3、rul算法需要一个限定储存数据的长度
LRUCache.java
public class LRUCache<V> {
private int capacity = 1024;
private Map<String,MyNode<String,V>> map = new ConcurrentHashMap<>();
private MyNode head;
private MyNode tail;
public LRUCache(){
this.head = new MyNode();
this.tail = new MyNode();
this.head.nextNode = tail;
this.tail.preNode = head;
}
//为方便测试以及动态控制容量
public LRUCache(int capacity){
this.head = new MyNode();
this.tail = new MyNode();
this.capacity = capacity;
this.head.nextNode = tail;
this.tail.preNode = head;
}
//
public V get(String key){
MyNode<String,V> curNode = map.get(key);
if(curNode==null){
return null;
}else if(curNode==head.nextNode){
//获取数据就是头部最新 数据,无需处理
return curNode.val;
}
//1 headNext 指向curNode.next
MyNode<String,V> headNext = head.nextNode;
headNext.nextNode = curNode.nextNode;
curNode.nextNode.preNode = headNext.nextNode;
//2 head.next 指向curNode
curNode.nextNode = headNext;
curNode.preNode = head;
head.nextNode = curNode;
return curNode.val;
}
public void put(String key,V v){
V gv = get(key);
if(gv==null){
//不存在的数据
//1、判断是否满容量 是则移除队尾数据 添加到队首 否则,只添加到队首
if(capacity>map.size()){
//1、判断是否满容量 ,未满,只添加到队首
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
map.put(key,newNode);
}else{
//2、判断是否满容量 ,已满,添加到队首、同时,移除队尾
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
//移除map中的尾部数据
MyNode<String,V> tailPre = tail.preNode;
map.remove(tailPre.key);
//移除队尾
expireTail();
map.put(key,newNode);
}
}else{
//存在数据
if(gv.equals(v)){
//一样的数据添加进去无需处理
}else{
//新数据代替老数据
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
//移除oldValue
MyNode<String,V> oldValue = map.get(key);
oldValue.preNode.nextNode = oldValue.nextNode;
oldValue.nextNode.preNode = oldValue.preNode;
oldValue = null;
}
}
}
/**
* 新增数据入队首
* @param key
* @param v
*/
private void intoHeadNext(MyNode<String,V> newNode,String key,V v){
newNode.nextNode = head.nextNode;
newNode.preNode = head;
head.nextNode.preNode=newNode;
head.nextNode = newNode;
}
private void expireTail(){
MyNode tailPre = tail.preNode;
tailPre.preNode.nextNode = tail;
tail.preNode = tailPre.preNode;
tailPre =null;
//help gc
}
private class MyNode<K,V>{
V val;
K key;
MyNode preNode;
MyNode nextNode;
}
preNode和nextNode构造成双向链表,如果还是不太清楚的同学,建议从简单链表重新加强学习,一起加油!
private class MyNode<K,V>{
V val;
K key;
MyNode preNode;
MyNode nextNode;
}
实例话的时候就应该构造好一个最简单的队列
public LRUCache(){
//给双向队列初始化一个默认的头节点 这个节点不能覆盖修改,方便后续操作
this.head = new MyNode();
//给双向列表一个默认的尾节点 ,不能修改、覆盖,同头节点
this.tail = new MyNode();
//构造双向链表
this.head.nextNode = tail;
this.tail.preNode = head;
}
思路:
MyNode<String,V> curNode = map.get(key);
public V get(String key){
MyNode<String,V> curNode = map.get(key);
if(curNode==null){
return null;
}else if(curNode==head.nextNode){
//获取数据就是头部最新 数据,无需处理
return curNode.val;
}
//1 headNext 指向curNode.next
MyNode<String,V> headNext = head.nextNode;
headNext.nextNode = curNode.nextNode;
curNode.nextNode.preNode = headNext.nextNode;
//2 head.next 指向curNode
curNode.nextNode = headNext;
curNode.preNode = head;
head.nextNode = curNode;
return curNode.val;
}
思路:
public void put(String key,V v){
V gv = get(key);
if(gv==null){
//不存在的数据
//1、判断是否满容量 是则移除队尾数据 添加到队首 否则,只添加到队首
if(capacity>map.size()){
//1、判断是否满容量 ,未满,只添加到队首
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
map.put(key,newNode);
}else{
//2、判断是否满容量 ,已满,添加到队首、同时,移除队尾
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
//移除map中的尾部数据
MyNode<String,V> tailPre = tail.preNode;
map.remove(tailPre.key);
//移除队尾
expireTail();
map.put(key,newNode);
}
}else{
//存在数据
if(gv.equals(v)){
//一样的数据添加进去无需处理
}else{
//新数据代替老数据
MyNode<String,V> newNode = new MyNode();
newNode.key=key;
newNode.val =v;
intoHeadNext(newNode,key,v);
//移除oldValue
MyNode<String,V> oldValue = map.get(key);
oldValue.preNode.nextNode = oldValue.nextNode;
oldValue.nextNode.preNode = oldValue.preNode;
oldValue = null;
}
}
}
/**
* 新增数据入队首
* @param newNode
* @param key
* @param v
*/
private void intoHeadNext(MyNode<String,V> newNode,String key,V v){
newNode.nextNode = head.nextNode;
newNode.preNode = head;
head.nextNode.preNode=newNode;
head.nextNode = newNode;
}
/**
* 过期队尾数据
*/
private void expireTail(){
MyNode tailPre = tail.preNode;
tailPre.preNode.nextNode = tail;
tail.preNode = tailPre.preNode;
tailPre =null;
//help gc
}
不知道大家看出代码的bug了吗,没有bug的代码,是不完美的代码
1、在put 方法中,先查询数据,此处用的get 方法,这里是不合适的,会做一次队列位置移动,应该使用 map.get(key)
2、在判断map.size()跟capacity的大小后,有不同的逻辑,此处不是原子操作,那问题来了,这个put get方法都有操作双向链表,这个操作其实都是线程不安全的。在单个线程中测试,是不能发现问题的。处理方案很简单,只需要在get put 方法添加上synchrocized关键字,但是性能就极大的降低了,大家可以再思考优化方案。
欢迎交流