■ 什么是LFU?
LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,也可以说是最近最少使用,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。 —来源: 百度百科
通俗点说就是有一个缓存,最多存三个数,现在已经有:a,b,c 三个数,然后他们分别被点击的次数为:3,1,2,然后现在有个新的数 d 要存入缓存中,这时因为缓存满了,要剔除掉一个数,根据LFU算法,会剔除掉最近最少使用的数,即字母 a,a剔除后,将 d 读入缓存,如果此时在没有点击任何数的情况下,又有一个数要存入缓存,则会将 d 给剔除。
■ 算法描述与实现?
设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
set(key,value)
:将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉(就是删除掉)。
get(key)
:返回key对应的value值。
实现:创建一个类用来存放对应的key值和命中次数,并且实现comparable类,使其能按照命中次数从小到大排序,接着用hashMap来存放对应的健值对,在缓存满的时候,通过删除命中次数最少的元素将新元素加入缓存中。
■ 代码实现?
public class LFU<K, V> extends HashMap<K, V> {
private static final int DEFAULT_MAX_SIZE = 5;
private int max_size = DEFAULT_MAX_SIZE;
private Map<K, HitRate> hashMap = new HashMap<>(); //变量类型是Map或HashMap都可以好像
public static void main(String[] args) {
LFU<String, String> cache = new LFU<>();
cache.put("a", "1");
cache.put("b", "2");
cache.put("c", "3");
cache.put("d", "4");
cache.put("e", "5");
//此时cache已经满,未定义最大容量,所以默认为5
System.out.println("当前缓存:" + cache.toString());
//此时a的命中次数为2
cache.get("a");
cache.get("a");
//删除命中次数最少的b,按照先进先出的意思,找到b为命中次数最少的
cache.put("f", "6");
System.out.println("放入f后的缓存:" + cache.toString());
cache.get("f"); //f命中+1
cache.get("c"); //c命中+1
cache.get("d");
cache.get("e");
cache.put("g", "7");
System.out.println("放入g后的缓存:" + cache.toString());
cache.put("h","8"); //删除命中最少的g
System.out.println("放入h后的缓存:" + cache.toString());
}
//空构造器,使用默认最大值容量
public LFU() {
this(DEFAULT_MAX_SIZE);
}
//按照传入容量定义容量的构造器
public LFU(int max_size) {
super(max_size);
this.max_size = max_size;
}
@Override
public V get(Object key) {
//返回指定键映射到的值 第一次get获得hash后对应的数组索引上
V v = super.get(key); //根据传入的key调用父级HashMap里的get将其hash()来获取对应的值
if (v != null) {
//第二次get取得当前数组索引位置上的HitRate结构上的元素 。并进行修改命中次数
HitRate hitRate = hashMap.get(key); //调用Map里的get(),拿到对应的HitRate结构
hitRate.hitCount += 1; //让命中次数加一
System.out.println(key + "被命中" + hitRate.hitCount + "次");
hitRate.atime = System.nanoTime(); //记录此次时间,纳秒为单位
}
//将获取到的值返回
return v;
}
@Override
public V put(K key, V value) {
//如果缓存满了,即缓存大小已经到达或超过最大时了
while (hashMap.size() >= max_size) {
K k = getLFUAge(); //取出命中最少的键
hashMap.remove(k);
this.remove(k); //从该映射中移除指定键的映射(如果存在)
}
//对于数据插入,这里要进行两次Hash去定位数据的存储位置
V v = super.put(key, value);
hashMap.put(key, new HitRate(key, 0, System.nanoTime()));
//增添键值信息
return v;
}
//获取缓存中被命中次数最少的
private K getLFUAge() {
//values()返回此映射中包含的值的集合视图
/***
* 根据给定集合元素的自然顺序返回该集合的最小元素。*集合中的所有元素都必须实现Comparable接口
*/
HitRate min = Collections.min(hashMap.values());
return min.key;
}
class HitRate implements Comparable<HitRate> {
private K key;
private Integer hitCount; //记录命中次数
private Long atime; //上次命中时间
public HitRate(K key, Integer hitCount, long atime) {
this.key = key;
this.hitCount = hitCount;
this.atime = atime;
}
@Override
public String toString() {
return "HitRate{" +
"key=" + key +
", hitCount=" + hitCount +
", atime=" + atime +
'}';
}
@Override
public int compareTo(HitRate o) {
int byHitCount = hitCount.compareTo(o.hitCount); //用命中次数来从小到大排序 相等则=0
return byHitCount != 0 ? byHitCount : atime.compareTo(atime); //如果没被命中且要排序就用上次命中时间排序
}
}
}
打印结果:
当前缓存:{a=1, b=2, c=3, d=4, e=5}
a被命中1次
a被命中2次
放入f后的缓存:{a=1, c=3, d=4, e=5, f=6}
f被命中1次
c被命中1次
d被命中1次
e被命中1次
放入g后的缓存:{a=1, d=4, e=5, f=6, g=7}
放入h后的缓存:{h=8, a=1, d=4, e=5, f=6}
♦ 总结
LFU也有自己的瑕疵的,比如在一个缓存页数总共为3的网页上,你从网页A->网页B->网页A->网页B->网页C->网页D,注意浏览网页C时,此时缓存已经满了,当你浏览网页D时,会将网页C即网页D的前一页剔除,此时你再返回上一页,网页的内容已经被清除,需要重新缓存,而不是像LRU那样,将最近最久没用的网页A删除,当然有利有弊,还需要自己斟酌使用哪一个,想了解LRF的实现可以参考下这篇文章:网页链接
❤ 加油