咱們都知道LRU是最近最少使用,根據數據的歷史訪問記錄來進行淘汰數據的。其核心思想是若是數據最近被訪問過,那麼未來訪問的概率也更高。在這裏提一下,Redis緩存和MyBatis二級緩存更新策略算法中就有LRU。畫外音:LFU是頻率最少使用,根據數據歷史訪問的頻率來進行淘汰數據。其核心思想是若是數據過去被訪問屢次,那麼未來訪問的概率也更高。node
其實一提到LRU,咱們就應該想到LinkedHashMap。LRU是經過雙向鏈表來實現的。當某個位置的數據被命中,經過調整該數據的位置,將其移動至尾部。新插入的元素也是直接放入尾部(尾插法)。這樣一來,最近被命中的元素就向尾部移動,那麼鏈表的頭部就是最近最少使用的元素所在的位置。算法
HashMap的afterNodeAccess()、afterNodeInsertion()、afterNodeRemoval()方法都是空實現,留着LinkedHashMap去重寫。LinkedHashMap靠重寫這3個方法就完成了核心功能的實現。不得不感嘆,HashMap和LinkedHashMap設計之妙。緩存
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
複製代碼
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
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;
}
}
複製代碼
在LinkedHashMap的get()方法中,咱們每次獲取元素的時候,都要調用afterNodeAccess(e)都要將元素移動到尾部。話外音:accessOrder爲true,是基於訪問排序,accessOrder爲基於插入排序。咱們想要LinkedHashMap實現LRU功能,accessOrder必須爲true。若是accessOrder爲false,那就是FIFO了。bash
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
複製代碼
咱們能夠看到插入數據的時候,若是removeEldestEntry(first)返回true,按照LRU策略,那麼會刪除頭節點。app
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
複製代碼
LinkedHashMap大致的LRU架子都爲咱們搭好了。那咱們怎麼去基於LinkedHashMap實現LRU呢。先別慌,咱們先看看MyBatis中的LruCache是怎麼實現的。ide
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
keyMap.get(key); //touch
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
private void cycleKeyList(Object key) {
keyMap.put(key, key);
if (eldestKey != null) {
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
}
複製代碼
咱們能夠照葫蘆畫瓢,來手寫LRU。其實咱們只要把accessOrder設置爲true,重寫removeEldestEntry(eldest)便可。咱們在removeEldestEntry(eldest)加上何時執行LRU操做的邏輯,好比map裏面的元素數量超過指定的大小,開始刪除最近最少使用的元素,爲後續新增的元素騰出位置來。post
咱們來看看本身手寫的LRU例子ui
1.首先往map裏面添加了5個元素,使用的是尾插法,順序應該是1,2,3,4,5。this
2.調用了map.put("6", "6")
,經過尾插法插入元素6,此時的順序是1,2,3,4,5,6,而後 LinkedHashMap調用removeEldestEntry(),map裏面的元素數量是6,大於指定的size,返回true。LinkedHashMap會刪除頭節點的元素,此時順序應該是2,3,4,5,6。spa
3.調用了map.get("2")
,元素2被命中,元素2須要移動到鏈表尾部,此時的順序是3,4,5,6,2
4.調用了map.put("7", "7")
,和步驟2同樣的操做。此時的順序是4,5,6,2,7
5.調用了map.get("4")
,和步驟3同樣的操做。此時的順序是5,6,2,7,4
@Test
public void test1() {
int size = 5;
/**
* false, 基於插入排序
* true, 基於訪問排序
*/
Map<String, String> map = new LinkedHashMap<String, String>(size, .75F,
false) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
System.out.println("最近最少使用的key=" + eldest.getKey());
}
return tooBig;
}
};
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.put("5", "5");
System.out.println(map.toString());
map.put("6", "6");
map.get("2");
map.put("7", "7");
map.get("4");
System.out.println(map.toString());
}
複製代碼
上面咱們是用LinkedHashMap裏面搭好的LRU架子來實現LRU的。如今咱們脫離LinkedHashMap這個容器,手動去維護鏈表中元素的關係,也就是仿照LinkedHashMap裏面的LRU實現寫出屬於本身的afterNodeRemoval()、afterNodeInsertion()、afterNodeAccess()方法。其實也是照着葫蘆畫瓢,只不過這一次難度升了幾顆星。
話外音:HashMap的查詢、插入、修改、刪除平均時間複雜度都是O(1)。最壞的狀況是全部的key都散列到一個Entry中,時間複雜度會退化成O(N)。這就是爲何Java8的HashMap引入了紅黑樹的緣由。當Entry中的鏈表長度超過8,鏈表會進化成紅黑樹。紅黑樹是一個自平衡二叉查找樹,它的查詢/插入/修改/刪除的平均時間複雜度爲O(log(N))。
1.首先咱們採用的是尾插法,也就是新插入的元素或者命中的元素往尾部移動,頭部的元素便是最近最少使用。
public class MyLru01<K, V> {
private int maxSize;
private Map<K, Entry<K, V>> map;
private Entry head;
private Entry tail;
public MyLru01(int maxSize) {
this.maxSize = maxSize;
map = new HashMap<>();
}
public void put(K key, V value) {
Entry<K, V> entry = new Entry<>();
entry.key = key;
entry.value = value;
afterEntryInsertion(entry);
map.put(key, entry);
if (map.size() > maxSize) {
map.remove(head.key);
afterEntryRemoval(head);
}
}
private void afterEntryInsertion(Entry<K, V> entry) {
if (entry != null) {
if (head == null) {
head = entry;
tail = head;
return;
}
if (tail != entry) {
Entry<K, V> pred = tail;
entry.before = pred;
tail = entry;
pred.after = entry;
}
}
}
private void afterEntryAccess(Entry<K, V> entry) {
Entry<K, V> last;
if ((last = tail) != entry) {
Entry<K, V> p = entry, b = p.before, a = p .after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
last = b;
} else {
a.before = b;
}
if (last == null) {
head = p;
} else {
p.before = last;
last.after = p;
}
tail = p;
}
}
private Entry<K, V> getEntry(K key) {
return map.get(key);
}
public V get(K key) {
Entry<K, V> entry = this.getEntry(key);
if (entry == null) {
return null;
}
afterEntryAccess(entry);
return entry.value;
}
public void remove(K key) {
Entry<K, V> entry = this.getEntry(key);
afterEntryRemoval(entry);
}
private void afterEntryRemoval(Entry<K, V> entry) {
if (entry != null) {
Entry<K, V> p = entry, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
tail = b;
} else {
a.before = b;
}
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
Entry<K, V> entry = head;
while (entry != null) {
sb.append(String.format("%s:%s", entry.key, entry.value));
sb.append(" ");
entry = entry.after;
}
return sb.toString();
}
static final class Entry<K, V> {
private Entry before;
private Entry after;
private K key;
private V value;
}
public static void main(String[] args) {
MyLru01<String, String> map = new MyLru01<>(5);
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.put("5", "5");
System.out.println(map.toString());
map.put("6", "6");
map.get("2");
map.put("7", "7");
map.get("4");
System.out.println(map.toString());
}
}
複製代碼
2.運行結果也是5,6,2,7,4,與以前用LinkedHashMap實現的LRU運行結果一致。後面會分析寫代碼的思路。
3.定義Entry中的雙向鏈表結構。
static final class Entry<K, V> {
private Entry before;
private Entry after;
private K key;
private V value;
}
複製代碼
4.把key,value包裝成Entry節點。調用afterEntryInsertion(entry)方法,把Entry節點移動到雙向鏈表尾部。而後將key,Entry放入到HashMap中。若是map中元素的數量大於maxSize,則刪除雙向鏈表中的頭結點(頭結點所在的元素就是最近最少使用的元素)。首先在map中刪除head.key對應着的元素,而後調用 afterEntryRemoval(head),在雙向鏈表中刪除頭節點。
public void put(K key, V value) {
Entry<K, V> entry = new Entry<>();
entry.key = key;
entry.value = value;
afterEntryInsertion(entry);
map.put(key, entry);
if (map.size() > maxSize) {
map.remove(head.key);
afterEntryRemoval(head);
}
}
複製代碼
5.若是雙向鏈表head節點爲空的話,證實雙向鏈表爲空。那麼咱們把新插入的元素置爲head節點和tail節點。不然咱們把插入當前節點至尾部。這裏是怎麼插入呢?tail節點以前是尾部節點,如今忽然要插入一個節點(entry節點)。那麼tail節點不再能佔據尾部的位置,咱們把置它爲pre節點。pre節點也就是新的tail節點(也就是entry節點)的前一個節點。entry的先驅節點指向pre,pre節點的後繼節點指向entry,這樣就完成了尾插入。
private void afterEntryInsertion(Entry<K, V> entry) {
if (entry != null) {
if (head == null) {
head = entry;
tail = head;
return;
}
if (tail != entry) {
Entry<K, V> pred = tail;
entry.before = pred;
tail = entry;
pred.after = entry;
}
}
}
複製代碼
6.咱們是怎麼在雙向鏈表中刪除一個節點呢?如今要刪除的節點是entry節點。咱們首先獲取它的先驅節點b和後繼節點a。若是b等於null,那麼刪除entry節點後,head節點應該爲a。若是b不等於null,b的後繼節點應該指向a。一樣若是a等於null,那麼刪除entry節點後,tail節點應該爲b。若是a不等於null,a的先驅節點應該指向b。這樣就完成刪除操做,若是還沒明白的話,本身拿個筆畫張圖就差很少了。
public void afterEntryRemoval(Entry<K, V> entry) {
if (entry != null) {
Entry<K, V> p = entry, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
tail = b;
} else {
a.before = b;
}
}
}
複製代碼
7.咱們經過get()方法命中了entry節點。那麼咱們怎麼把entry節點移動至雙向鏈表中的尾部呢?若是當前節點已位於尾部,那麼咱們什麼也不作。若是當前節點不在尾部,和上面操做同樣首先獲取它的先驅節點b和後繼節點a。而後把先驅節點和後繼節點都置爲null,方便後續操做。
若是b節點等於null,那麼移動entry節點至尾部後,head節點應該爲a節點。
若是b節點不等於null,那麼b的後繼節點應該指向a。
若是a節點等於null,那麼新的尾部節點的前一個節點應該爲b。
若是a節點不等於null,那麼a的先驅節點應該指向b。
若是last節點(也就是新尾部節點的前一個節點)等於null的話,說明head節點應該爲p節點。
若是last節點不等於null的話,咱們把p的先驅節點指向last,last的後繼節點指向p。最後新的尾部節點就是p。
過程有點繞,若是不明白的話,能夠動手畫圖。
private void afterEntryAccess(Entry<K, V> entry) {
Entry<K, V> last;
if ((last = tail) != entry) {
Entry<K, V> p = entry, b = p.before, a = p .after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
last = b;
} else {
a.before = b;
}
if (last == null) {
head = p;
} else {
p.before = last;
last.after = p;
}
tail = p;
}
}
複製代碼
頭插法其實和尾插法大同小異,區別就是新插入的節點或者是命中的節點都移動至雙向鏈表的頭部,那麼雙向鏈表的尾部節點中所在的元素就是最近最少使用的元素。
頭插法的代碼實現和尾插法基本一致,只是afterEntryInsertion()和afterEntryAccess()方法有所改動。改動的地方其實能夠用上面的文字歸納了!
再來講說下面例子中元素位置變化的過程吧 1.由於頭插入法,5個元素插入完畢後。順序應該是5,4,3,2,1
2.執行map.put("6", "6")
後,把元素6插入到頭部,並刪除掉尾部元素1,順序是6,5,4,3,2。
3.執行map.get("2")
後,將元素2移動到頭部,順序是2,6,5,4,3
4.執行map.put("7", "7")
後,把元素7插入到頭部,並刪除掉尾部元素,3,順序是7,2,6,5,4
5.執行map.get("4")
後,把元素4移動到頭部,最後的順序是4,7,2,6,5
/**
* @author cmazxiaoma
* @version V1.0
* @Description: TODO
* @date 2018/9/3 9:19
*/
public class MyLru02<K, V> {
private int maxSize;
private Map<K, Entry<K, V>> map;
private Entry<K, V> head;
private Entry<K, V> tail;
public MyLru02(int maxSize) {
this.maxSize = maxSize;
map = new HashMap<>();
}
public void put(K key, V value) {
Entry<K, V> entry = new Entry<>();
entry.key = key;
entry.value = value;
afterEntryInsertion(entry);
map.put(key, entry);
if (map.size() > maxSize) {
map.remove(tail.key);
afterEntryRemoval(tail);
}
}
public void afterEntryInsertion(Entry<K, V> entry) {
if (entry != null) {
if (head == null) {
head = entry;
tail = head;
return;
}
// if entry is not head
if (head != entry) {
entry.after = head;
entry.before = null;
head.before = entry;
head = entry;
}
}
}
public void afterEntryRemoval(Entry<K, V> entry) {
if (entry != null) {
Entry<K, V> p = entry, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
tail = b;
} else {
a.before = b;
}
}
}
public void afterEntryAccess(Entry<K, V> entry) {
Entry<K, V> first;
if ((first = head) != entry) {
Entry<K, V> p = entry, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) {
first = a;
} else {
b.after = a;
}
if (a == null) {
tail = b;
} else {
a.before = b;
}
if (first == null) {
tail = p;
} else {
p.after = first;
first.before = p;
}
head = p;
}
}
public void remove(K key) {
Entry<K, V> entry = this.getEntry(key);
afterEntryRemoval(entry);
}
public V get(K key) {
Entry<K, V> entry = this.getEntry(key);
if (entry == null) {
return null;
}
afterEntryAccess(entry);
return entry.value;
}
private Entry<K, V> getEntry(K key) {
Entry<K, V> entry = map.get(key);
if (entry == null) {
return null;
}
return entry;
}
@Override
public String toString() {
Entry<K, V> p = head;
StringBuffer sb = new StringBuffer();
while(p != null) {
sb.append(String.format("%s:%s", p.key, p.value));
sb.append(" ");
p = p.after;
}
return sb.toString();
}
static final class Entry<K, V> {
private Entry<K, V> before;
private Entry<K, V> after;
private K key;
private V value;
}
public static void main(String[] args) {
MyLru02<String, String> map = new MyLru02<>(5);
map.put("1", "1");
map.put("2", "2");
map.put("3", "3");
map.put("4", "4");
map.put("5", "5");
System.out.println(map.toString());
map.put("6", "6");
map.get("2");
map.put("7", "7");
map.get("4");
System.out.println(map.toString());
}
}
複製代碼
你們好,我是cmazxiaoma(寓意是沉夢昂志的小馬),感謝各位閱讀本文章。 小弟不才。 若是您對這篇文章有什麼意見或者錯誤須要改進的地方,歡迎與我討論。 若是您以爲還不錯的話,但願大家能夠點個贊。 但願個人文章對你能有所幫助。 有什麼意見、看法或疑惑,歡迎留言討論。
最後送上:心之所向,素履以往。生如逆旅,一葦以航。