Java實現常用緩存淘汰算法:FIFO、LRU、LFU
緩存淘汰算法
在高並發、高性能的質量要求不斷提高時,我們首先會想到的就是利用緩存予以應對。
第一次請求時把計算好的結果存放在緩存中,下次遇到同樣的請求時,把之前保存在緩存中的數據直接拿來使用。
但是,緩存的空間一般都是有限,不可能把所有的結果全部保存下來。那麼,當緩存空間全部被占滿再有新的數據需要被保存,就要決定刪除原來的哪些數據。如何做這樣決定需要使用緩存淘汰算法。
常用的緩存淘汰算法有:FIFO、LRU、LFU,下面我們就逐一介紹一下。
FIFO
FIFO,First In First Out,先進先出算法。判斷被存儲的時間,離目前最遠的數據優先被淘汰。簡單地說,先存入緩存的數據,先被淘汰。
最早存入緩存的數據,其不再被使用的可能性比剛存入緩存的可能性大。建立一個FIFO隊列,記錄所有在緩存中的數據。當一條數據被存入緩存時,就把它插在隊尾上。需要被淘汰的數據一直在隊列頭。這種算法隻是在按線性順序訪問數據時才是理想的,否則效率不高。因為那些常被訪問的數據,往往在緩存中也停留得最久,結果它們卻因變“老”而不得不被淘汰出去。
FIFO算法用隊列實現就可以瞭,這裡就不做代碼實現瞭。
LRU
LRU,Least Recently Used,最近最少使用算法。判斷最近被使用的時間,目前最遠的數據優先被淘汰。簡單地說,LRU 的淘汰規則是基於訪問時間。
如果一個數據在最近一段時間沒有被使用到,那麼可以認為在將來它被使用的可能性也很小。因此,當緩存空間滿時,最久沒有使用的數據最先被淘汰。
在Java中,其實LinkedHashMap已經實現瞭LRU緩存淘汰算法,需要在構造函數第三個參數傳入true,表示按照時間順序訪問。可以直接繼承LinkedHashMap來實現。
package one.more; import java.util.LinkedHashMap; import java.util.Map; public class LruCache<K, V> extends LinkedHashMap<K, V> { /** * 容量限制 */ private int capacity; LruCache(int capacity) { // 初始大小,0.75是裝載因子,true是表示按照訪問時間排序 super(capacity, 0.75f, true); //緩存最大容量 this.capacity = capacity; } /** * 重寫removeEldestEntry方法,如果緩存滿瞭,則把鏈表頭部第一個節點和對應的數據刪除。 */ @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } }
我寫一個簡單的程序測試一下:
package one.more; public class TestApp { public static void main(String[] args) { LruCache<String, String> cache = new LruCache(3); cache.put("keyA", "valueA"); System.out.println("put keyA"); System.out.println(cache); System.out.println("========================="); cache.put("keyB", "valueB"); System.out.println("put keyB"); System.out.println(cache); System.out.println("========================="); cache.put("keyC", "valueC"); System.out.println("put keyC"); System.out.println(cache); System.out.println("========================="); cache.get("keyA"); System.out.println("get keyA"); System.out.println(cache); System.out.println("========================="); cache.put("keyD", "valueD"); System.out.println("put keyD"); System.out.println(cache); } }
運行結果如下:
put keyA
{keyA=valueA}
=========================
put keyB
{keyA=valueA, keyB=valueB}
=========================
put keyC
{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
{keyC=valueC, keyA=valueA, keyD=valueD}
當然,這個不是面試官想要的,也不是我們想要的。我們可以使用雙向鏈表和哈希表進行實現,哈希表用於存儲對應的數據,雙向鏈表用於數據被使用的時間先後順序。
在訪問數據時,如果數據已存在緩存中,則把該數據的對應節點移到鏈表尾部。如此操作,在鏈表頭部的節點則是最近最少使用的數據。
當需要添加新的數據到緩存時,如果該數據已存在緩存中,則把該數據對應的節點移到鏈表尾部;如果不存在,則新建一個對應的節點,放到鏈表尾部;如果緩存滿瞭,則把鏈表頭部第一個節點和對應的數據刪除。
package one.more; import java.util.HashMap; import java.util.Map; public class LruCache<K, V> { /** * 頭結點 */ private Node head; /** * 尾結點 */ private Node tail; /** * 容量限制 */ private int capacity; /** * key和數據的映射 */ private Map<K, Node> map; LruCache(int capacity) { this.capacity = capacity; this.map = new HashMap<>(); } public V put(K key, V value) { Node node = map.get(key); // 數據存在,將節點移動到隊尾 if (node != null) { V oldValue = node.value; //更新數據 node.value = value; moveToTail(node); return oldValue; } else { Node newNode = new Node(key, value); // 數據不存在,判斷鏈表是否滿 if (map.size() == capacity) { // 如果滿,則刪除隊首節點,更新哈希表 map.remove(removeHead().key); } // 放入隊尾節點 addToTail(newNode); map.put(key, newNode); return null; } } public V get(K key) { Node node = map.get(key); if (node != null) { moveToTail(node); return node.value; } return null; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("LruCache{"); Node curr = this.head; while (curr != null) { if(curr != this.head){ sb.append(',').append(' '); } sb.append(curr.key); sb.append('='); sb.append(curr.value); curr = curr.next; } return sb.append('}').toString(); } private void addToTail(Node newNode) { if (newNode == null) { return; } if (head == null) { head = newNode; tail = newNode; } else { //連接新節點 tail.next = newNode; newNode.pre = tail; //更新尾節點指針為新節點 tail = newNode; } } private void moveToTail(Node node) { if (tail == node) { return; } if (head == node) { head = node.next; head.pre = null; } else { //調整雙向鏈表指針 node.pre.next = node.next; node.next.pre = node.pre; } node.pre = tail; node.next = null; tail.next = node; tail = node; } private Node removeHead() { if (head == null) { return null; } Node res = head; if (head == tail) { head = null; tail = null; } else { head = res.next; head.pre = null; res.next = null; } return res; } class Node { K key; V value; Node pre; Node next; Node(K key, V value) { this.key = key; this.value = value; } } }
再次運行測試程序,結果如下:
put keyA
LruCache{keyA=valueA}
=========================
put keyB
LruCache{keyA=valueA, keyB=valueB}
=========================
put keyC
LruCache{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
LruCache{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
LruCache{keyC=valueC, keyA=valueA, keyD=valueD}
LFU
LFU,Least Frequently Used,最不經常使用算法,在一段時間內,數據被使用次數最少的,優先被淘汰。簡單地說,LFU 的淘汰規則是基於訪問次數。
如果一個數據在最近一段時間很少被使用到,那麼可以認為在將來它被使用的可能性也很小。因此,當空間滿時,最小頻率使用的數據最先被淘汰。
我們可以使用雙哈希表進行實現,一個哈希表用於存儲對應的數據,另一個哈希表用於存儲數據被使用次數和對應的數據。
package one.more; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class LfuCache<K, V> { /** * 容量限制 */ private int capacity; /** * 當前最小使用次數 */ private int minUsedCount; /** * key和數據的映射 */ private Map<K, Node> map; /** * 數據頻率和對應數據組成的鏈表 */ private Map<Integer, List<Node>> usedCountMap; public LfuCache(int capacity) { this.capacity = capacity; this.minUsedCount = 1; this.map = new HashMap<>(); this.usedCountMap = new HashMap<>(); } public V get(K key) { Node node = map.get(key); if (node == null) { return null; } // 增加數據的訪問頻率 addUsedCount(node); return node.value; } public V put(K key, V value) { Node node = map.get(key); if (node != null) { // 如果存在則增加該數據的訪問頻次 V oldValue = node.value; node.value = value; addUsedCount(node); return oldValue; } else { // 數據不存在,判斷鏈表是否滿 if (map.size() == capacity) { // 如果滿,則刪除隊首節點,更新哈希表 List<Node> list = usedCountMap.get(minUsedCount); Node delNode = list.get(0); list.remove(delNode); map.remove(delNode.key); } // 新增數據並放到數據頻率為1的數據鏈表中 Node newNode = new Node(key, value); map.put(key, newNode); List<Node> list = usedCountMap.get(1); if (list == null) { list = new LinkedList<>(); usedCountMap.put(1, list); } list.add(newNode); minUsedCount = 1; return null; } } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("LfuCache{"); List<Integer> usedCountList = this.usedCountMap.keySet().stream().collect(Collectors.toList()); usedCountList.sort(Comparator.comparingInt(i -> i)); int count = 0; for (int usedCount : usedCountList) { List<Node> list = this.usedCountMap.get(usedCount); if (list == null) { continue; } for (Node node : list) { if (count > 0) { sb.append(',').append(' '); } sb.append(node.key); sb.append('='); sb.append(node.value); sb.append("(UsedCount:"); sb.append(node.usedCount); sb.append(')'); count++; } } return sb.append('}').toString(); } private void addUsedCount(Node node) { List<Node> oldList = usedCountMap.get(node.usedCount); oldList.remove(node); // 更新最小數據頻率 if (minUsedCount == node.usedCount && oldList.isEmpty()) { minUsedCount++; } node.usedCount++; List<Node> set = usedCountMap.get(node.usedCount); if (set == null) { set = new LinkedList<>(); usedCountMap.put(node.usedCount, set); } set.add(node); } class Node { K key; V value; int usedCount = 1; Node(K key, V value) { this.key = key; this.value = value; } } }
再次運行測試程序,結果如下:
put keyA
LfuCache{keyA=valueA(UsedCount:1)}
=========================
put keyB
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1)}
=========================
put keyC
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1)}
=========================
get keyA
LfuCache{keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1), keyA=valueA(UsedCount:2)}
=========================
put keyD
LfuCache{keyC=valueC(UsedCount:1), keyD=valueD(UsedCount:1), keyA=valueA(UsedCount:2)}
總結
看到這裡,你已經超越瞭大多數人!
FIFO,First In First Out,先進先出算法。判斷被存儲的時間,離目前最遠的數據優先被淘汰,可以使用隊列實現。
LRU,Least Recently Used,最近最少使用算法。判斷最近被使用的時間,目前最遠的數據優先被淘汰,可以使用雙向鏈表和哈希表實現。
LFU,Least Frequently Used,最不經常使用算法,在一段時間內,數據被使用次數最少的,優先被淘汰,可以使用雙哈希表實現。
以上就是Java實現常用緩存淘汰算法:FIFO、LRU、LFU的詳細內容,更多關於Java緩存淘汰算法的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 手動實現Redis的LRU緩存機制示例詳解
- Java之理解Redis回收算法LRU案例講解
- 淺談java如何實現Redis的LRU緩存機制
- Java 手寫LRU緩存淘汰算法
- 如何利用Go語言實現LRU Cache