Java LRU 與 LFU 算法及應(yīng)用小結(jié)
在Java面試和日常開發(fā)中,LRU和LFU是出現(xiàn)頻率最高的兩種緩存淘汰算法。它們的核心區(qū)別在于淘汰依據(jù):LRU看“時間”(最近誰沒用過),LFU看“頻率”(誰最不常用)。
1. LRU(Least Recently Used,最近最少使用)
核心思想
淘汰最長時間未被訪問的數(shù)據(jù)。假設(shè)“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的概率也更高”。
Java實(shí)現(xiàn)方式
方式一:LinkedHashMap一行搞定(最推薦)
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
public LRUCache(int capacity) {
// accessOrder=true 表示按訪問順序排序
super(capacity, 0.75f, true);
this.maxCapacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity; // 容量超標(biāo)時移除最老元素
}
}使用示例:
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
cache.get(1); // 訪問1,使其變?yōu)樽钚?
cache.put(4, "D"); // 觸發(fā)淘汰,移除最久未用的2
System.out.println(cache); // {3=C, 1=A, 4=D}方式二:手寫雙向鏈表 + HashMap(面試??迹?/h4>
class LRUCacheManual {
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
DLinkedNode() {}
DLinkedNode(int key, int value) { this.key = key; this.value = value; }
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size, capacity;
private DLinkedNode head, tail;
public LRUCacheManual(int capacity) {
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node); // 移到鏈表頭部(表示最近使用)
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
} else {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
size--;
}
}
}
// 省略 addToHead, removeTail, moveToHead 等鏈表操作...
}
class LRUCacheManual {
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
DLinkedNode() {}
DLinkedNode(int key, int value) { this.key = key; this.value = value; }
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size, capacity;
private DLinkedNode head, tail;
public LRUCacheManual(int capacity) {
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node); // 移到鏈表頭部(表示最近使用)
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
} else {
DLinkedNode newNode = new DLinkedNode(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DLinkedNode tail = removeTail();
cache.remove(tail.key);
size--;
}
}
}
// 省略 addToHead, removeTail, moveToHead 等鏈表操作...
}2. LFU(Least Frequently Used,最不經(jīng)常使用)
核心思想
淘汰訪問次數(shù)最少的數(shù)據(jù)。如果訪問次數(shù)相同,則淘汰最久未使用的(需要結(jié)合時間)。
實(shí)現(xiàn)難點(diǎn)
LFU比LRU復(fù)雜得多,因?yàn)樾枰瑫r維護(hù)頻率和時間兩個維度。LeetCode 460題就是典型。
經(jīng)典實(shí)現(xiàn)(雙哈希表 + 頻率鏈表)
class LFUCache {
private Map<Integer, Node> keyToNode; // key -> 數(shù)據(jù)節(jié)點(diǎn)
private Map<Integer, LinkedHashSet<Node>> freqToKeys; // 頻率 -> 該頻率下的key集合(用LinkedHashSet保證順序)
private int minFreq; // 當(dāng)前最小頻率
private int capacity;
class Node {
int key, value, freq;
Node(int key, int value) {
this.key = key;
this.value = value;
this.freq = 1; // 初始頻率為1
}
}
public LFUCache(int capacity) {
this.capacity = capacity;
keyToNode = new HashMap<>();
freqToKeys = new HashMap<>();
minFreq = 0;
}
public int get(int key) {
if (!keyToNode.containsKey(key)) return -1;
Node node = keyToNode.get(key);
updateFreq(node); // 訪問后頻率+1
return node.value;
}
public void put(int key, int value) {
if (capacity == 0) return;
if (keyToNode.containsKey(key)) {
Node node = keyToNode.get(key);
node.value = value;
updateFreq(node);
} else {
if (keyToNode.size() >= capacity) {
// 淘汰最小頻率中最久未使用的那個
LinkedHashSet<Node> set = freqToKeys.get(minFreq);
Node oldest = set.iterator().next();
set.remove(oldest);
keyToNode.remove(oldest.key);
if (set.isEmpty()) freqToKeys.remove(minFreq);
}
Node newNode = new Node(key, value);
keyToNode.put(key, newNode);
freqToKeys.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(newNode);
minFreq = 1; // 新插入的一定是頻率最低
}
}
private void updateFreq(Node node) {
int oldFreq = node.freq;
// 從舊頻率集合中移除
LinkedHashSet<Node> oldSet = freqToKeys.get(oldFreq);
oldSet.remove(node);
if (oldSet.isEmpty()) {
freqToKeys.remove(oldFreq);
if (minFreq == oldFreq) minFreq++;
}
// 放入新頻率集合
node.freq++;
freqToKeys.computeIfAbsent(node.freq, k -> new LinkedHashSet<>()).add(node);
}
}3. 應(yīng)用場景對比
| 維度 | LRU | LFU |
|---|---|---|
| 適用場景 | 熱點(diǎn)數(shù)據(jù)有明顯時效性(如用戶最近瀏覽) | 熱點(diǎn)數(shù)據(jù)穩(wěn)定且長期(如熱門視頻、全局配置) |
| 典型例子 | Redis內(nèi)存淘汰(allkeys-lru)、Glide圖片緩存、瀏覽器后退按鈕 | 數(shù)據(jù)庫查詢緩存(統(tǒng)計高頻SQL)、CDN內(nèi)容緩存 |
| 優(yōu)點(diǎn) | 實(shí)現(xiàn)簡單,性能高,符合局部性原理 | 能過濾掉偶發(fā)性突發(fā)流量,保留真正高頻數(shù)據(jù) |
| 缺點(diǎn) | 容易被批量訪問"污染"(一次全表掃描會把所有舊數(shù)據(jù)擠出去) | 實(shí)現(xiàn)復(fù)雜,維護(hù)成本高;新數(shù)據(jù)剛進(jìn)來頻率低,容易被立即淘汰(需加老化機(jī)制) |
4. 生產(chǎn)環(huán)境中的最佳實(shí)踐
① 直接使用成熟緩存框架
不要自己寫LFU,99%的場景用Google的Caffeine就夠了:
// Caffeine 同時支持 LRU 和 LFU(實(shí)際是 TinyLFU,LFU的增強(qiáng)版)
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();Caffeine使用的TinyLFU(論文算法)結(jié)合了LFU和LRU的優(yōu)點(diǎn),并解決了新數(shù)據(jù)"饑餓"問題。
② Redis中的使用
- LRU:Redis默認(rèn)近似LRU(非精確,采樣淘汰),配置
maxmemory-policy allkeys-lru - LFU:Redis 4.0+支持,配置
allkeys-lfu,適合穩(wěn)定的熱點(diǎn)數(shù)據(jù)緩存
③ 數(shù)據(jù)庫連接池或線程池
使用LinkedBlockingDeque實(shí)現(xiàn)簡單LRU,淘汰長時間空閑的連接。
5. 面試高頻追問
Q:為什么LinkedHashMap能實(shí)現(xiàn)LRU?
A:構(gòu)造參數(shù)accessOrder=true使鏈表按訪問順序排序,每次get/put都會將節(jié)點(diǎn)移到尾部,頭部就是最久未用的。重寫removeEldestEntry控制淘汰時機(jī)。
Q:LRU和LFU哪個更優(yōu)?
A:沒有絕對優(yōu)劣。突發(fā)流量場景(如雙11秒殺)用LRU更合適;穩(wěn)態(tài)熱點(diǎn)場景(如排行榜)用LFU更精準(zhǔn)。工業(yè)界常用兩者混合(如Arc算法、TinyLFU)。
Q:如何解決LFU中"新數(shù)據(jù)剛來就被淘汰"的問題?
A:給新數(shù)據(jù)初始頻率賦一個較高的值(如初始頻率=5),或使用窗口計數(shù)(只統(tǒng)計最近一段時間內(nèi)的訪問次數(shù)),Caffeine的TinyLFU就是這么做的。
到此這篇關(guān)于Java LRU 與 LFU 算法及應(yīng)用小結(jié)的文章就介紹到這了,更多相關(guān)Java LRU LFU內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot創(chuàng)建maven多模塊項(xiàng)目實(shí)戰(zhàn)代碼
本篇文章主要介紹了SpringBoot創(chuàng)建maven多模塊項(xiàng)目實(shí)戰(zhàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09
Springboot自定義mvc組件如何實(shí)現(xiàn)
這篇文章主要介紹了Springboot自定義mvc組件如何實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11
Java調(diào)用第三方http接口的四種方式總結(jié)
這篇文章主要給大家介紹了關(guān)于Java調(diào)用第三方http接口的四種方式,在實(shí)際開發(fā)中我們經(jīng)常會與第三方公司進(jìn)行合作,接入第三方接口,文中給出了詳細(xì)的代碼實(shí)例,需要的朋友可以參考下2023-08-08
如何通過SpringBoot實(shí)現(xiàn)商城秒殺系統(tǒng)
這篇文章主要介紹了如何通過SpringBoot實(shí)現(xiàn)商城秒殺系統(tǒng),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
Springboot實(shí)現(xiàn)郵件發(fā)送功能
這篇文章主要為大家詳細(xì)介紹了Springboot實(shí)現(xiàn)郵件發(fā)送功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-02-02

