LRU介绍
LRU是Least Recently Used 最近最少使用算法。是一种常用的内存管理的页面置换算法。
计算机中用缓存来存放以前读取的数据,而不是直接丢掉,这样,再次读取的时候,可以直接在缓存里面取,而不用再重新查找一遍,这样系统的反应能力会有很大提高。但是,当我们读取的个数特别大的时候,我们不可能把所有已经读取的数据都放在缓存里,毕竟内存大小是一定的,我们一般把最近常读取的放在缓存里。
LRU就是实现了这种思想,也就是在缓存中把最近未使用的向后移让位与最新进入的数据。
我们可以简单的用一个链表实现这样一种思想。因为访问的元素就要移动到表头,用链表实现插入移出只用O(1)时间,而数组移动就需要O(N)的时间,所以用链表实现最简单且最高效。当读取一个数据时就将其加入到队列头部,如果是从队列中读取数据,还要把数据从元位置删除并将其加入头部。这样当缓存不够时就从队尾删除数据。这样就可以简单的实现LRU思想。
LinkedHashMap简述
LinkedHashMap实现了两种元素的排序方式:插入排序和访问排序。
这个访问排序不就是实现了上述的LRU思想吗?
LinkedHashMap继承自HashMap,可能各位要想,HashMap是一个Map,而上述我们要实现的明显是一个链表。Map如何有链表的功能呢。接下来我们来看看LinkedHashMap的具体实现
源码解读
private static class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
......
}
private final boolean accessOrder;
private transient Entry<K,V> header;
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
这时LinkedHashMap的两个属性,从这里可以看到要建链表的迹象啊。还有数据排序方式,不过这个排序方式默认是插入模式。
LinkedHashMap的节点继承自HashMap.Entry,不过新加入了before和after属性,这就使得我们的排序链表中前后的元素就能关联起来。而HashMap.Entry中也有next属性,这是HashMap处理hash碰撞后所用到的下一个元素的引用。
这时HashMap的示例图,图中的小箭头就是next。但是你能想想不在同一行的节点也能通过after和before连在一起吗?这个链表就是我们的LinkedHashMap的链表。
LinkedHashMap的存取
LinkedHashMap并未实现put方法,而是实现put()需要用到的addEntry()方法。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//如果put的元素Map中本来就有,就要按照排序方式进行调整。
e.recordAccess(this);//此方法是在LinkedHashMap中实现
return oldValue;
}
}
modCount++;
//如果put的元素要新加入到map中,就要用子类的方法
addEntry(hash, key, value, i);//LinkedHashMap重写这个方法
return null;
}
上述方法还是要将节点加入到上图所示的HashMap中的数组中。只不过各个节点按照LinkedHashMap的排序方式,维护了before和after的属性。
下面我们来看一看如果put的元素已经存在于map中,即访问map中原有的元素,LinkedHashMap如果实现调整。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
//accessOrder默认是false,即按插入顺序。如果是访问顺序就要进行下面操作
lm.modCount++;
remove();//在原队列中删除当前节点
addBefore(lm.header);//移动当前节点
}
}
private void remove() {
//修改前后节点的after和before引用,将当前节点从当前位置去除
before.after = after;
after.before = before;
}
private void addBefore(Entry<K,V> existingEntry) {
//添加到队首,双向队列,哪来的队首??,哈哈
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
凭借上面的三个方法,就可以将LinkedHashMap维护的队列进行处理,如果accessOrder是插入顺序,就不做任何处理。如果按照访问顺序,按照LRU原则,访问的元素要放在最前面,所以要更改当前节点的after和before引用以达到更改位置的目的。
如果put的元素map中没有,put循环结束就进入到addEntry()方法。
void addEntry(int hash, K key, V value, int bucketIndex) {
createEntry(hash, key, value, bucketIndex);
Entry<K,V> eldest = header.after;//队尾元素
if (removeEldestEntry(eldest)) {//永远返回false,即不删除链表元素
removeEntryForKey(eldest.key);
} else {
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
HashMap中的addEntry()方法
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
两个类将新的元素加入到map的方法还是有很多相似的地方,毕竟都是加入HashMap维护的数组中。不同的是,LinkedHashMap要维护自己的队列,要维护自己队列中的after和before属性。
LinkedHashMap中的removeEldestEntry()方法永远返回false,这样做的本意为链表是没有size限制的,如果我们要做LRU,就可以重写这个方法让其返回true。
而LinkedHashMap中的get()方法只是比HashMap中多了一个维护自己链表的工作而已
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);//调用HashMap的方法
if (e == null)
return null;
e.recordAccess(this);//维护自己的队列
return e.value;
}
总结
LinkedHashMap继承自HashMap,但是一个Map无论如何也是实现不了链表的功能。所以LinkedHashMap自己维护了一个双向链表。这个双向链表按照我们想要的顺序把HashMap中的元素串联起来实现链表。也就是说,数据的存储还是按照HashMap的方式存放,但是LinkedHashMap中维护了一个链表将所有的元素串联起来。