面試題目:手寫一個LRU演算法實現

奕鋒部落格發表於2022-04-01

一、常見的記憶體淘汰演算法

  • FIFO  先進先出

    • 在這種淘汰演算法中,先進⼊快取的會先被淘汰

    • 命中率很低

  • LRU

    • Least recently used,最近最少使⽤get

    • 根據資料的歷史訪問記錄來進⾏淘汰資料,其核⼼思想是“如果資料最近被訪問過,那麼將來被訪問的⼏率也更⾼”

    • LRU演算法原理剖析

  • LFU   

    • Least Frequently Used
    • 演算法根據資料的歷史訪問頻率來淘汰資料,其核⼼思想是“如果資料過去被訪問多次,那麼將來被訪問的頻率也更⾼”

    • LFU演算法原理剖析

      • 新加⼊資料插⼊到佇列尾部(因為引⽤計數為1)

      • 佇列中的資料被訪問後,引⽤計數增加,佇列重新排序;

      • 當需要淘汰資料時,將已經排序的列表最後的資料塊刪除。

 

  • LFU的缺點
    • 複雜度
    • 儲存成本
    • 尾部容易被淘汰

二、手寫LRU演算法實現

利用了LinkedHashMap雙向連結串列插入可排序

@Slf4j
public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private int cacheSize;

    public LRUCache(int cacheSize) {
        super(16, 0.75f, true);
        this.cacheSize = cacheSize;
    }

    @Override
    public synchronized V get(Object key) {
        return super.get(key);
    }

    @Override
    public synchronized V put(K key, V value) {
        return super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        boolean f = size() > cacheSize;
        if (f) {
            log.info("LRUCache清除第三方金鑰快取Key:[{}]", eldest.getKey());
        }
        return f;
    }

    public static void main(String[] args) {
        LRUCache<String, Object> cache = new LRUCache<>(5);
        cache.put("A","A");
        cache.put("B","B");
        cache.put("C","C");
        cache.put("D","D");
        cache.put("E","E");
        System.out.println("初始化:" + cache.keySet());
        System.out.println("訪問值:" + cache.get("C"));
        System.out.println("訪問C後:" + cache.keySet());
        System.out.println("PUT F後:" + cache.put("F","F"));
        System.out.println(cache.keySet());
    }

}

main函式執行效果:

三、注意事項

LinkedHashMap有五個建構函式

面試題目:手寫一個LRU演算法實現
//使用父類中的構造,初始化容量和載入因子,該初始化容量是指陣列大小。
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
//一個引數的構造
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
//無參構造
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
//這個不用多說,用來接受map型別的值轉換為LinkedHashMap
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }
//真正有點特殊的就是這個,多了一個引數accessOrder。儲存順序,LinkedHashMap關鍵的引數之一就在這個,
  //true:指定迭代的順序是按照訪問順序(近期訪問最少到近期訪問最多的元素)來迭代的。 false:指定迭代的順序是按照插入順序迭代,也就是通過插入元素的順序來迭代所有元素
//如果你想指定訪問順序,那麼就只能使用該構造方法,其他三個構造方法預設使用插入順序。
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
View Code

  引數accessOrder。儲存順序,LinkedHashMap關鍵的引數之一就在這個, true:指定迭代的順序是按照訪問順序(近期訪問最少到近期訪問最多的元素)來迭代的。 false:指定迭代的順序是按照插入順序迭代,也就是通過插入元素的順序來迭代所有元素。

  如果你想指定訪問順序,那麼就只能使用該構造方法,其他三個構造方法預設使用插入順序。

 public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

LinkedHashMap是非執行緒安全的,需要加互斥鎖解決併發問題。

四、思考

  需要根據應用場景確定cacheSize大小,如果實際快取數量過小,會導致快取中的資料長期得不到重新整理,為防止這種或偶發情況的發生,可配合定時任務如起一個newSingleThreadScheduledExecutor,將上面儲存的value修改封裝為一個物件,裡面增加一個時間戳儲存,每次訪問實時更新,定時掃描該佇列將最近30分鐘未訪問的key刪除;還需增加一個初始進入佇列的歷史時間記錄,將超過1小時的資料清除。

 

相關文章