1. 程式人生 > >LRU、LFU演算法java實現

LRU、LFU演算法java實現

近期使用springboot整合ehcache實現快取,spring還支援使用簡單ConcurrentMapCache實現,底層就是用ConcurrentHashMap實現。ehcache相對來說比較重,加pom依賴下載了很長時間,但是ehcache有很多可配置的選項,其中包括快取達到一定大小淘汰的演算法的選擇。包括了FIFO、LRU、LFU可以根據不同的業務場景選擇。

一、LRU的實現比較簡單,因為java中的LinkedHashMap有很多特點正好適合LRU的思想。LRU(least recently used)最近最少使用,首先淘汰最長時間未被使用的頁面。使用連結串列實現,當一個元素被訪問了,把元素移動到連結串列的頂端,插入新元素也放到頂端,當快取數量到達限制,直接從連結串列底端移除元素。

public class LRU<k, v> extends LinkedHashMap<k, v> {

  private final int MAX_SIZE;

  public LRU(int capcity) {
    super(8, 0.75f,true);
    this.MAX_SIZE = capcity;
  }

  @Override
  public boolean removeEldestEntry(Map.Entry<k, v> eldest) {
    if (size() > MAX_SIZE) {
      System.out
.println("移除的元素為:" + eldest.getValue()); } return size() > MAX_SIZE; } public static void main(String[] args) { Map<Integer, Integer> map = new LRU<>(5); for (int i = 1; i <= 11; i++) { map.put(i, i); System.out.println("cache的容量為:" + map.size()); if
(i == 4) { map.get(1); } } System.out.println("=-=-=-=-=-=-=-map元素:"); map.entrySet().forEach(integerIntegerEntry -> System.out.println(integerIntegerEntry.getValue())); } }

LinkedHashMap有一個accessOrder的引數正好和LRU的思路相契合,這裡使用0.75的預設載入因子(載入因子過小空間利用率低,衝突減少,訪問速度快,載入因子過大,反之。載入因子過大,當執行put操作,兩個物件的hashcode相同時要操作連結串列,相應的get操作是也要操作連結串列,這樣就使得訪問變慢。)
removeEldestEntry方法當結果返回為true時,它會清除map中的最老元素。以實現LRU的演算法。
執行結果為:

cache的容量為:1
cache的容量為:2
cache的容量為:3
cache的容量為:4
cache的容量為:5
移除的元素為:2
cache的容量為:5
移除的元素為:3
cache的容量為:5
移除的元素為:4
cache的容量為:5
移除的元素為:1
cache的容量為:5
移除的元素為:5
cache的容量為:5
移除的元素為:6
cache的容量為:5
=-=-=-=-=-=-=-map元素:
7
8
9
10
11

程式碼裡面為了測試,在加入元素4的時候,訪問了一下元素1,然後看到,在快取達到限制時,最先移除的不是1,而是2,3然後是1。

二、LFU(Least Frequently Used)淘汰一定時期內被訪問次數最少的元素。如果元素的一定時間內的訪問次數相同時,則比較他們的最新一次的訪問時間。

public class LFU<k, v> {
  private final int capcity;

  private Map<k, v> cache = new HashMap<>();

  private Map<k, HitRate> count = new HashMap<>();

  public LFU(int capcity) {
    this.capcity = capcity;
  }

  public void put(k key, v value) {
    v v = cache.get(key);
    if (v == null) {
      if (cache.size() == capcity) {
        removeElement();
      }
      count.put(key, new HitRate(key, 1, System.nanoTime()));
    } else {
      addHitCount(key);
    }
    cache.put(key, value);
  }

  public v get(k key) {
    v value = cache.get(key);
    if (value != null) {
      addHitCount(key);
      return value;
    }
    return null;
  }

  //移除元素
  private void removeElement() {
    HitRate hr = Collections.min(count.values());
    cache.remove(hr.key);
    count.remove(hr.key);
  }

  //更新訪問元素狀態
  private void addHitCount(k key) {
    HitRate hitRate = count.get(key);
    hitRate.hitCount = hitRate.hitCount + 1;
    hitRate.lastTime = System.nanoTime();
  }

  //內部類
  class HitRate implements Comparable<HitRate> {
    private k key;
    private int hitCount;
    private long lastTime;

    private HitRate(k key, int hitCount, long lastTime) {
      this.key = key;
      this.hitCount = hitCount;
      this.lastTime = lastTime;
    }

    @Override
    public int compareTo(HitRate o) {
      int compare = Integer.compare(this.hitCount, o.hitCount);
      return compare == 0 ? Long.compare(this.lastTime, o.lastTime) : compare;
    }
  }


  public static void main(String[] args) {
    LFU<Integer, Integer> cache = new LFU<>(3);
    cache.put(2, 2);
    cache.put(1, 1);

    System.out.println(cache.get(2));
    System.out.println(cache.get(1));
    System.out.println(cache.get(2));

    cache.put(3, 3);
    cache.put(4, 4);

    //1、2元素都有訪問次數,放入3後快取滿,加入4時淘汰3
    System.out.println(cache.get(3));
    System.out.println(cache.get(2));
    //System.out.println(cache.get(1));
    System.out.println(cache.get(4));

    cache.put(5, 5);
    //目前2訪問2次,1訪問一次,4訪問一次,由於4的時間比較新,放入5的時候移除1元素。
    System.out.println("-=-=-=-");
    cache.cache.entrySet().forEach(entry -> {
      System.out.println(entry.getValue());
    });

  }
}

這裡實現略微複雜,首先要維持一個快取的map還要維持一個訪問次數以及時間的map。
1、首先內部類有3個屬性,有快取的key,快取的訪問次數,最近一次的訪問時間。內部實現比較器,按照先訪問次數後時間的比較順序。整個類作為物件放在一個hashmap的value裡。
2、put方法:當一個元素要放入快取的時候,先去快取檢查是否有相同的key,也就是要填加的(key,value)在cache的map裡get(key)是否為空。這裡不用檢查value是否有相同的,標識一個快取的唯一性是key,key不同就是一個不同的快取元素。
當get(key)為空時,要放入快取新元素了,首先檢查快取的容量是否達到限制值,達到了,執行移除一個元素的方法,然後在count的map里加入相應的訪問資訊,初始值為1。如果沒達到限制值,直接在count的map里加入相應的訪問資訊,初始值為1。
當get(key)不為空直接執行addHitCount方法。將訪問次數加1,更新最新訪問時間。
最後不管什麼情況,執行map.put方法。

執行結果:

2
1
2
null
2
4
-=-=-=-
2
4
5

開始加入1,2,訪問1,一次2兩次。加入3,在加入4.發現快取元素為124.原因是1,2都有訪問次數,3和4在時間上,比較古老,會被淘汰。

兩種實現方法是在各大部落格上學習後,理解後,自己小小的改造了一下所總結,謝謝各位大神。