<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【死磕 Java Core】 — 自己動手實現(xiàn)一個 LRU

          共 9041字,需瀏覽 19分鐘

           ·

          2021-07-28 07:51

          LRU,即 Least Recently Use ,直譯為 “最近最少使用”。它是根據(jù)數(shù)據(jù)的歷史訪問記錄來進行數(shù)據(jù)淘汰的,淘汰掉最先訪問的數(shù)據(jù),其核心思想是 如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也會更加高。

          要實現(xiàn) LRU,需要做到兩點:

          • 查詢出最近最晚使用的項
          • 給最近使用的項做一個標記

          實現(xiàn)的方案有多種,這里小編主要介紹兩種:

          1. LinkedHashMap
          2. 雙向鏈表 + HashMap

          LinkedHashMap 實現(xiàn)

          利用 LinkedHashMap 的原因就在于 LinkedHashMap 是有序的,默認情況下是按照元素的添加順序存儲的,也可以調(diào)整為根據(jù)訪問順序來調(diào)整內(nèi)部順序(設置參數(shù) accessOrder 進行調(diào)整),即最近讀取的數(shù)據(jù)放在最前面,我們就是利用 LinkedHashMap 的這個特性來實現(xiàn) LRU。先來一個簡單的例子吧:

              public static void main(String[] args){
          Map<String,String> map = new LinkedHashMap(10,0.75f,true);

          map.put("1","a");
          map.put("2","b");
          map.put("3","c");
          map.put("4","d");

          System.out.println("原始順序為:");
          for(Iterator<Map.Entry<String,String>> it = map.entrySet().iterator();it.hasNext();){
          System.out.print(it.next().getKey() + " ");
          }
          System.out.println();

          map.get("2");

          System.out.println("訪問 4 之后的順序為:");
          for(Iterator<Map.Entry<String,String>> it = map.entrySet().iterator();it.hasNext();){
          System.out.print(it.next().getKey() + " ");
          }
          }

          運行結果:

          原始順序為:
          1 2 3 4
          訪問 4 之后的順序為:
          1 3 4 2

          更多關于 LinkedHashMap,請看這篇文章:圖解集合6:LinkedHashMap

          LinkedHashMap 實現(xiàn) LRU 有兩種方式,一種是繼承 LinkedHashMap,一種是利用組合的方式,下面分別演示這兩種情況。

          繼承 LinkedHashMap

          采用繼承的方式實現(xiàn)起來是非常簡單的,因為 LinkedHashMap 本身就已經(jīng)具備了 LRU 的特性,我們只需要實現(xiàn)一點:當容器中元素個數(shù)超過我們設定的容量后,刪除第一個元素即可。同時由于 LinkedHashMap 本身不具備線程安全,我們需要確保他線程安全,這個也很簡單,重寫 LinkedHashMap 的 get()put() 方法即可,或者使用 Collections.synchronizedMap() 方法也可以。實現(xiàn)如下:

          public class LRUCacheLinkedHashMap<K,V> extends LinkedHashMap<K,V> {

          /**
          * 定一緩存容量
          */

          private int capacity;

          LRUCacheLinkedHashMap(int capacity){
          // AccessOrder = true
          super(capacity,0.75f,true);

          this.capacity = capacity;
          }

          /**
          * 實現(xiàn)LRU的關鍵方法,如果 map 里面的元素個數(shù)大于了緩存最大容量,則刪除鏈表的頂端元素
          *
          * @param eldest
          * @return
          */

          @Override
          public boolean removeEldestEntry(Map.Entry<K, V> eldest){
          System.out.println(eldest.getKey() + "=" + eldest.getValue());
          return size()>capacity;
          }

          @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);
          }
          }

          驗證

             public static void main(String[] args){
          LRUCacheLinkedHashMap cache = new LRUCacheLinkedHashMap(5);

          cache.put("1","a");
          cache.put("2","b");
          cache.put("3","c");
          cache.put("4","d");
          cache.put("5","e");

          System.out.println("插入 5 個元素后的順序");
          printlnCache(cache);

          // 插入第 6 個元素
          cache.put("6","e");

          System.out.println("插入第 6 個元素后的順序");
          printlnCache(cache);

          // 訪問 第 3 個元素
          cache.get("3");

          System.out.println("訪問元素 3 后的順序");
          printlnCache(cache);

          }

          private static void printlnCache(LRUCacheLinkedHashMap cacheMap){
          for(Iterator<Map.Entry<String,String>> it = cacheMap.entrySet().iterator(); it.hasNext();){
          System.out.print(it.next().getKey() + " ");
          }
          System.out.println();
          }

          運行結果:

          插入 5 個元素后的順序
          1 2 3 4 5
          插入第 6 個元素后的順序
          2 3 4 5 6
          訪問元素 3 后的順序
          2 4 5 6 3

          運行結果完全符合我們的預期

          組合 LinkedHashMap

          使用組合的方式可能會更加優(yōu)雅些,但是由于沒有實現(xiàn) Map 接口,所以就不能使用 Collections.synchronizedMap() 方式來保證線程安全性了,所以需要在每個方法處增加 synchronized 來確保線程安全。實現(xiàn)方式如下:

          public class LRUCache<K,V> {
          private int capacity;

          private Map<K,V> cacheMap;

          public LRUCache(int capacity){
          this.capacity = capacity;

          cacheMap = new LinkedHashMap<>(capacity,0.75f,true);
          }

          public synchronized void put(K k,V v){
          cacheMap.put(k,v);

          // 移除第一個元素
          if(cacheMap.size() > capacity){
          K first = this.keyIterator().next();

          cacheMap.remove(first);
          }
          }

          public synchronized V get(K k){
          return cacheMap.get(k);
          }

          public Iterator<K> keyIterator(){
          return cacheMap.keySet().iterator();
          }
          }

          驗證:

              public static void main(String[] args) {
          LRUCache lruCache = new LRUCache(5);

          lruCache.put("1","a");
          lruCache.put("2","b");
          lruCache.put("3","c");
          lruCache.put("4","d");
          lruCache.put("5","e");

          System.out.println("插入 5 個元素后的順序");
          println(lruCache);

          // 插入第 6 個元素
          lruCache.put("6","e");

          System.out.println("插入 第 6 個元素后的順序");
          println(lruCache);

          // 訪問 第 3 個元素
          lruCache.get("3");

          System.out.println("訪問元素 3 后的順序");
          println(lruCache);

          }

          private static void println(LRUCache lruCache){
          for(Iterator it = lruCache.keyIterator(); it.hasNext();){
          System.out.print(it.next() + " ");
          }
          System.out.println();
          }

          運行結果如下:

          插入 5 個元素后的順序
          1 2 3 4 5
          插入 第 6 個元素后的順序
          2 3 4 5 6
          訪問元素 3 后的順序
          2 4 5 6 3

          組合的方式也顯得非常簡單,有兩點需要注意:

          1. 保證每個方法的線程安全
          2. put 時,需要查看當前容量是否超過設置的容量,超過則需要刪除第一個元素。當然小編這種是實現(xiàn)方式不是很優(yōu)雅,這么做知識為了能夠更加好闡述 LRU 的實現(xiàn)。更好的方案是在構造 LinkedHashMap 時,重寫 removeEldestEntry(),如下:
                  cacheMap = new LinkedHashMap<K,V>(capacity,0.75f,true){
          @Override
          protected boolean removeEldestEntry(Map.Entry eldest) {
          return size()>capacity;
          }
          };

          鏈表 + HashMap 實現(xiàn)

          我們想想,在不利用現(xiàn)存數(shù)據(jù)結構的條件(如 LinkedHashMap)如何實現(xiàn)一個 LRU 呢?緩存部分容易實現(xiàn),我們都知道利用 HashMap 即可,但是如何實現(xiàn)緩存容量不足時丟棄最不常用的數(shù)據(jù)的功能?

          • 利用時間戳。每一個訪問,增加的元素我們都給其更新一個時間戳,在 put 的時候,檢查,刪除時間戳最小的就可以了。這種方法可以實現(xiàn),但是代價較高,就是我們需要遍歷整個數(shù)據(jù),得到最小的時間戳。
          • 我們可以換位思考,我們其實不需要關注每個節(jié)點的增加或者遍歷時間,我們只需要知道那個節(jié)點是最先訪問就可以了,所以我們可以利用鏈表記錄訪問記錄,有新數(shù)據(jù)加入時放在鏈表的 head 節(jié)點,每次訪問也將該數(shù)據(jù)放在 head 節(jié)點,那么鏈表的 tail 一定是最早訪問的節(jié)點,所以每次當容量不足的時候刪除 tail 節(jié)點數(shù)據(jù)并將它的前驅(qū)節(jié)點設置為 tail 就可以了。注意,這個鏈表是一個雙向鏈表。代碼如下:
          public class LinkedLRUCache<K,V> {

          private int capacity;

          private Map<K,LRUNode> map;

          private LRUNode head;

          private LRUNode tail;

          LinkedLRUCache(int capacity){
          this.capacity = capacity;
          this.map = new HashMap<>();
          }

          public synchronized void put(K k,V v){
          LRUNode node = map.get(k);

          // 存在該 key,將節(jié)點的設置為 head
          if(node != null){
          node.value = v;

          remove(node,false);
          }else{
          /**
          * 該節(jié)點不存在
          * 1、將該節(jié)點加入緩存
          * 2、設置該節(jié)點為 head
          * 3、判斷是否超出容量
          */

          node = new LRUNode(k,v);

          if(map.size() >= capacity){
          //刪除 tail 節(jié)點
          remove(tail,true);
          }

          map.put(k,node);

          setHead(node);
          }

          // 設置當前節(jié)點為首節(jié)點
          setHead(node);
          }

          public Object get(String key) {
          LRUNode node = map.get(key);
          if (node != null) {
          // 將剛操作的元素放到head
          remove(node, false);
          setHead(node);
          return node.value;
          }
          return null;
          }

          /**
          * 設置頭結點
          *
          * @param node
          */

          private void setHead(LRUNode node) {
          if(head != null){
          node.next = head;
          head.prev = node;
          }

          head = node;

          if(tail == null){
          tail = node;
          }
          }

          /**
          * 從鏈表中刪除此Node
          *
          * @param node
          * @param flag 為 true 就刪除該節(jié)點的 key
          */

          private void remove(LRUNode node,boolean flag) {
          if (node.prev != null) {
          node.prev.next = node.next;
          } else {
          head = node.next;
          }
          if (node.next != null) {
          node.next.prev = node.prev;
          } else {
          tail = node.prev;
          }
          node.next = null;
          node.prev = null;
          if (flag) {
          map.remove(node.key);
          }
          }

          private Iterator iterator(){
          return map.keySet().iterator();
          }

          private class LRUNode<K,V> {

          /**
          * cache 的 key
          */

          private K key;

          /**
          * cache 的 value
          */

          private V value;

          private LRUNode next;

          private LRUNode prev;

          LRUNode(K key, V value) {
          this.key = key;
          this.value = value;
          }
          }
          }

          驗證

             public static void main(String[] args){
          LRUCache lruCache = new LRUCache(5);

          lruCache.put("1","a");
          lruCache.put("2","b");
          lruCache.put("3","c");
          lruCache.put("4","d");
          lruCache.put("5","e");

          System.out.println("插入 5 個元素");
          println(lruCache);

          System.out.println("插入 3 元素");
          lruCache.put("3","c");
          println(lruCache);

          System.out.println("插入第 6 個元素");
          lruCache.put("6","f");
          println(lruCache);

          System.out.println("訪問 4 元素");
          lruCache.get("4");
          println(lruCache);
          }

          private static void println(LRUCache lruCache){
          Iterator iterator = lruCache.keyIterator();
          while (iterator.hasNext()){
          System.out.print(iterator.next() + " ");
          }

          System.out.println();
          }

          執(zhí)行結果:

          插入 5 個元素
          1 2 3 4 5
          插入 3 元素
          1 2 4 5 3
          插入第 6 個元素
          2 4 5 3 6
          訪問 4 元素
          2 5 3 6 4


          瀏覽 76
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  se999se| 免费无码视频在线播放 | 伊人大香蕉在线视频网 | 无码精品人妻一区二区三蜜桃 | 亚洲色图欧美色图在线观看 |