LRU(最近最久未使用)
当缓存满了的时候,我们要淘汰出最久未被使用的数据。
1.用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。这需要每次遍历hash表,不是很好。
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
HashMap h = new HashMap<Integer,Integer>();
int count=0;
while(in.hasNext()){
//每次进来都将每个数的时间戳加1
if(h.size()>0){
Set<Integer> s1 = h.keySet();
for (int key : s1) {
h.put(key, ((int)h.get(key))+1);
}
}
//存入的值
int a = in.nextInt();
//将要存入的值时间戳置0
h.put(a, 0);
//将超过3个的数中最近最久未使用的值删除,这里用时间戳来判断
//其中时间戳越大,说明最近最久未使用
if(h.size()>3){
Set<Integer> s = h.keySet();
int k=0;
int max = 0;
for (int key : s) {
if((int)h.get(key) >= max){
k = key;
max = (int)h.get(key);
}
}
h.remove(k);
System.out.println(k);
}
}
}
}
2.利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。同样需要不停的遍历双向链表
public class Demo1 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
LinkedList list = new LinkedList<Integer>();
while(in.hasNext()){
int a = in.nextInt();
//如果包含要插入的数据,将该数据移到双向链表的头部(先删除原来位置数据,再加到头部)
if(list.contains(a)){
for (Object o : list) {
if((int)o==a){
list.remove(o);
}
}
list.addFirst(a);
}else{
//未包含,则加入链表头部
list.addFirst(a);
//如果链表长度大于3,则删除链表尾部的元素
if(list.size()>3){
System.out.println(list.removeLast());
}
}
}
}
}
3.利用链表和hashmap。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
使用LinkedHashMap实现
LinkedHashMap底层就是用的HashMap加双链表实现的,而且本身已经实现了按照访问顺序的存储。此外,LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数,方法默认是直接返回false,不会移除元素,所以需要重写该方法。即当缓存满后就移除最不常用的数。
public class Demo1 {
public static void main(String[] args) {
LinkedHashMap lk = new LinkedHashMap(){
@Override
protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
// 假设内存缓存数位3
return size()>3;
};
};
lk.put(7, "7");
lk.put(0,"0");
lk.put(1, "1");
lk.put(0,"9");
lk.put(9,"5");
Set s = lk.entrySet();
for (Object object : s) {
System.out.println(object);
}
}
}
扩展
1.LRU-K
LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。
数据第一次被访问时,加入到历史访问列表,如果书籍在访问历史列表中没有达到K次访问,则按照一定的规则(FIFO,LRU)淘汰;当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列中删除,将数据移到缓存队列中,并缓存数据,缓存队列重新按照时间排序;缓存数据队列中被再次访问后,重新排序,需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即“淘汰倒数K次访问离现在最久的数据”。
LRU-K具有LRU的优点,同时还能避免LRU的缺点,实际应用中LRU-2是综合最优的选择。由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多。
2.two queue
Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。
新访问的数据插入到FIFO队列中,如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;如果数据在FIFO队列中再次被访问到,则将数据移到LRU队列头部,如果数据在LRU队列中再次被访问,则将数据移动LRU队列头部,LRU队列淘汰末尾的数据。
3.Multi Queue(MQ)
MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。详细的算法结构图如下,Q0,Q1....Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:
新插入的数据放入Q0,每个队列按照LRU进行管理,当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列中删除,加入到高一级队列的头部;为了防止高优先级数据永远不会被淘汰,当数据在指定的时间里没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;需要淘汰数据时,从最低一级队列开始按照LRU淘汰,每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部。如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列头部。Q-history按照LRU淘汰数据的索引。
MQ需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。