面試是一場與面試官交心的過程,會遇到一些成熟穩重的大牛、同樣也會遇到一些設計挖苦你自以爲是的人,這些都不重要,我們能夠做到的只有好好掌握知識,一點點的積累。
LRU( Least Recently Used ) 算法,經常會在面試中問到,雖然名字聽起來高大上,但是算法其實很簡單,最近最少使用的就將其排除在列表之外,以便將最近最常使用的節點放在列表最前面,在取數據的時候方便快捷的拿到數據,提高性能,其實說到這裏,聰明的你應該都知道了其中的原理,下面我們通過其中的源碼進行分析。
面試題:LRU算法實現原理以及在項目中的應用
上面說到,最近最少使用將排除在列表之外,那這個列表是什麼?? 通過源碼得知,jdk中實現的LRU算法內部持有了一個LinkedHashMap:
/**
*一個基於LinkedHashMap的LRU緩存淘汰算法,
* 這個緩存淘汰列表包含了一個最大的節點值,如果在列表滿了之後再有額外的值添加進來,
* 則LRU(最近最少使用)的節點將被移除列表外
*
* 當前類是線程安全的,所有的方法都被同步了
*/
public class LRUCache<K, V> {
private static final float hashTableLoadFactor = 0.75f;
private LinkedHashMap<K, V> map;
private int cacheSize;
//...
}
而LinkedHashMap內部節點的特性就是一個雙向鏈表,有頭結點和尾節點,有下一個指針節點和上一個指針節點;LRUCache中,主要是put() 與 get () 兩個方法,LinkedHashMap是繼承至HashMap,查看源碼得知LinkedHashMap並沒有重寫父類的put方法,而是實現了其中另外一個方法,afterNodeInsertion() 接下來分析一下LinkedHashMap中的put() 方法:
// 由LinkedHashMap 實現回調
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
// HashMap.put()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
// 回調給LinkedHashMap ,evict爲boolean
afterNodeInsertion(evict);
return null;
}
可以看到新增節點的方法,是由父類實現,並傳遞迴調函數afterNodeInsertion(evict) 給LinkedHashMap實現:
void afterNodeInsertion(boolean evict) { // 可能移除最老的節點
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
可以看到 if 中有一個removeEldestEntry(first) 方法,該方法是給用戶去實現的,該怎樣去移除這個節點,最常用的是判斷當前列表的長度是否大於緩存節點的長度:
this.map = new LinkedHashMap<K, V>(hashTableCapacity, LRUCache.hashTableLoadFactor, true) {
// (an anonymous inner class)
private static final long serialVersionUID = 1;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 返回爲true的話可能移除節點
return this.size() > LRUCache.this.cacheSize;
}
};
接下來就是get() 方法了:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
// 調用HashMap給LinkedHashMap的回調方法
afterNodeAccess(e);
return e.value;
}
// afterNodeAccess(e)
void afterNodeAccess(Node<K,V> e) { // move node to last 將節點移到最後
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
所以在這個方法中,就將我們上面說到的最近最常使用的節點移到最後,而最近最少使用的節點自然就排列到了前面,如果需要移除的話,就從前面刪除掉了節點。
總結
說了這麼多,又是LRUCache使用,又是LinkedHashMap使用,面試的時候,我跟面試官說這麼多源碼有啥用啊? 其實看完源碼,我們對原理應該瞭解得很清晰了,LRU算法就是淘汰算法,其中內置了一個LinkedHashMap來存儲數據。
比如我們的圖片加載框架Glide,其中大部分算法都是LRU算法;有內存緩存算法和磁盤緩存算法(DiskLRUCache); 當緩存的內存達到一定限度時,就會從列表中移除圖片(當然Glide有活動內存和內存兩個,並不是直接刪除掉)。
面試的時候,說到LinkedHashMap中的幾個HashMap回調方法與其中操作,再聯繫一些常用的框架及具體實現,相信會事倍功半吧^~^