LRU, En Az Son Kullanılan için kısadır ve kelimenin tam anlamıyla En Az Son Kullanılan anlamına gelir.
Genellikle önbellek eleme stratejisinin uygulanması için kullanılır, çünkü önbellek çok değerli olduğundan, hafızanın dolu olmadığından emin olmak için verileri belirli kurallara göre elemek gerekir.
Örneğin, yaygın olarak kullanılan Redis aşağıdaki stratejilere sahiptir:
Strateji açıklaması Volatile-lru, volatile-ttl'yi ortadan kaldırmak için son kullanma süresi ayarlı veri kümesinden en az son kullanılan verileri seçer. allkeys-lru, tüm veri kümelerinden en son kullanılan verileri seçer ve tüm anahtarları ortadan kaldırır - rastgele olarak tüm veri kümelerinden verileri ortadan kaldırmak için seçer Kıskançlık, verilerin atılmasını yasaklar
Birini başarın
Daha önce bir röportaj sorusuna da maruz kaldım, muhtemelen gereksinimler şunlardır:
Aşağıdaki benim uygulamam:
public class LRUAbstractMap, java.util.AbstractMap { özel son statik Logger LOGGER = LoggerFactory.getLogger (LRUAbstractMap.class); / ** * İpliğin gecikmiş olup olmadığını kontrol edin * / özel ExecutorService checkTimePool; / ** * harita maksimum boyutu * / özel nihai statik int MAX_SIZE = 1024; özel nihai statik ArrayBlockingQueue < Düğüm > QUEUE = new ArrayBlockingQueue < > (MAX_SIZE); / ** * Varsayılan boyut * / özel nihai statik int DEFAULT_ARRAY_SIZE = 1024; / ** * Dizi uzunluğu * / özel int arraySize; / ** * Dizi * / özel Nesne dizileri; / ** * Bayrağın durdurulup durdurulmayacağını belirleyin * / özel uçucu boole bayrağı = true; / ** * fazla mesai * / özel nihai statik Uzun EXPIRE_TIME = 60 * 60 * 1000L; / ** * Haritanın tamamının boyutu * / özel uçucu AtomicInteger boyutu; public LRUAbstractMap () { arraySize = DEFAULT_ARRAY_SIZE; diziler = yeni Nesne; // Sıraya konulan ilk değerin güncelliğini yitirmiş olup olmadığını kontrol etmek için bir iş parçacığı açın executeCheckTime (); } / ** * Sıraya konulan ilk değerin güncel olup olmadığını kontrol etmek için bir iş parçacığı başlatın ve onu bir arka plan iş parçacığı olarak ayarlayın * / private void executeCheckTime () { ThreadFactory namedThreadFactory = yeni ThreadFactoryBuilder () .setNameFormat ("kontrol-iş parçacığı-% d") .setDaemon (doğru) .inşa etmek(); checkTimePool = new ThreadPoolExecutor (1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue < > (1), namedThreadFactory, yeni ThreadPoolExecutor.AbortPolicy ()); checkTimePool.execute (yeni CheckTimeThread ()); } @Override genel Set < Giriş > entrySet () { dönüş super.keySet (); } @Override public Object put (Nesne anahtarı, Nesne değeri) { int hash = hash (anahtar); int indeks = hash% arraySize; Düğüm currentNode = (Düğüm) dizileri; if (currentNode == null) { diziler = yeni Düğüm (boş, boş, anahtar, değer); // Sırayı yaz QUEUE.offer ((Düğüm) dizileri); genişlemek(); }Başka { Düğüm cNode = currentNode; Düğüm nNode = cNode; // Varsa üzerine yaz eğer (nNode.key == anahtar) { cNode.val = değer; } while (nNode.next! = null) { // anahtar var, basit yargının üzerine yaz eğer (nNode.key == anahtar) { nNode.val = değer; kırmak; }Başka { // Mevcut değilse bağlantılı bir liste ekleyin genişlemek(); Düğüm düğümü = yeni Düğüm (düğüm, boş, anahtar, değer); // Sırayı yaz QUEUE.offer (currentNode); cNode.next = düğüm; } nNode = nNode.next; } } boş döndür; } @Override public Object get (Nesne anahtarı) { int hash = hash (anahtar); int indeks = hash% arraySize; Düğüm currentNode = (Düğüm) dizileri; if (currentNode == null) { boş döndür; } eğer (currentNode.next == null) { //Güncelleme zamanı currentNode.setUpdateTime (System.currentTimeMillis ()); //Çatışma yok currentNode döndür; } Düğüm nNode = currentNode; while (nNode.next! = null) { eğer (nNode.key == anahtar) { //Güncelleme zamanı currentNode.setUpdateTime (System.currentTimeMillis ()); dönüş düğümü; } nNode = nNode.next; } super.get (anahtar) döndür; } @Override public Nesne kaldırma (Nesne anahtarı) { int hash = hash (anahtar); int indeks = hash% arraySize; Düğüm currentNode = (Düğüm) dizileri; if (currentNode == null) { boş döndür; } eğer (currentNode.key == key) { sizeDown (); diziler = boş; // Sırayı kaldırın QUEUE.poll (); currentNode döndür; } Düğüm nNode = currentNode; while (nNode.next! = null) { eğer (nNode.key == anahtar) { sizeDown (); // Bağlantılı listede bulundu, önceki düğümün sonraki düğümünü geçerli düğümün sonraki düğümüne yönlendir nNode.pre.next = nNode.next; nNode = boş; // Sırayı kaldırın QUEUE.poll (); dönüş düğümü; } nNode = nNode.next; } dönüş super.remove (anahtar); } / ** * Boyutu büyüt * / private void sizeUp () { // Değer koyduğunuzda, içinde zaten veri olduğunu düşünün bayrak = doğru; eğer (size == null) { size = new AtomicInteger (); } int size = this.size.incrementAndGet (); eğer (boyut > = MAX_SIZE) { // Sıranın başındaki verileri bulun Düğüm düğümü = QUEUE.poll (); if (node == null) { yeni RuntimeException ("veri hatası") atar; } // Anahtarı çıkarın Nesne anahtarı = node.key; kaldır (anahtar); lruCallback (); } } / ** * Miktarda azalma * / private void sizeDown () { eğer (KUYRUK.size () == 0) { bayrak = yanlış; } this.size.decrementAndGet (); } @Override public int size () { dönüş boyutu.get (); } / ** * Bağlantılı liste * / özel sınıf Node { sonraki özel Düğüm; özel Düğüm ön; özel Nesne anahtarı; özel Nesne değeri; özel Uzun updateTime; genel Düğüm (Ön düğüm, Sonraki düğüm, Nesne anahtarı, Nesne değeri) { this.pre = pre; this.next = sonraki; this.key = anahtar; this.val = val; this.updateTime = System.currentTimeMillis (); } public void setUpdateTime (Long updateTime) { this.updateTime = updateTime; } public Long getUpdateTime () { updateTime döndür; } @Override public String toString () { "Düğüm {"'ü döndür + "anahtar =" + anahtar + ", val =" + val + '}'; } } / ** * HashMap kopyasının karma uygulaması * @param anahtarı * @dönüş * / public int hash (Nesne anahtarı) { int h; return (key == null)? 0: (h = key.hashCode ()) ^ (h > > > 16); } private void lruCallback () { LOGGER.debug ("lruCallback"); } özel sınıf CheckTimeThread Runnable { @Override public void runru {n
süre (bayrak) { Deneyin { Düğüm düğümü = QUEUE.poll (); if (node == null) { devam et; } Uzun updateTime = node.getUpdateTime (); eğer ((updateTime-System.currentTimeMillis ()) > = EXPIRE_TIME) { remove (node.key); } } catch (İstisna e) { LOGGER.error ("InterruptedException"); } } } } }İlgilenen arkadaşlar doğrudan şunları yapabilir:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUAbstractMap.java
Yerel olarak çalıştırmak için kodu indirin.
Kod daha fazla görünüyor, ancak uygulama fikri hala nispeten basit:
Yukarıdaki kod genellikle işlevseldir, ancak ölümcül bir sorun vardır.
Son zamanlarda En az kullanılan Memnuniyet yoksa, silinen veriler girilen ilk verilerdir.
Bununla birlikte, get get süreci, HashMap'in biraz anlaşılmasını derinleştirebilecek basit bir HashMap uygulaması olarak kabul edilebilir.Gerçekleşme iki
Öyleyse, bu sefer sona erme süresini dikkate almadan eksiksiz bir LRU önbelleği nasıl uygulanır.
Aslında, önceki uygulamadan bazı fikirler de düşünülebilir:
Yukarıdaki iki noktaya dayanarak, yaygın olarak kullanılan bir veri yapısını düşünmek kolaydır: Bağlantılı liste .
Bu nedenle, aşağıdaki uygulama:
genel sınıf LRUMap < K, V > { özel final Haritası < K, V > cacheMap = yeni HashMap < > (); / ** * Maksimum önbellek boyutu * / özel int cacheSize; / ** * Düğüm boyutu * / özel int nodeCount; / ** * Baş düğüm * / özel Düğüm < K, V > başlık; / ** * Son düğüm * / özel Düğüm < K, V > döşeme ustası; public LRUMap (int cacheSize) { this.cacheSize = cacheSize; // Baş düğümün sonraki düğümü boş başlık = yeni Düğüm < > (); header.next = boş; // Son düğümün son düğümü boş tailer = yeni Düğüm < > (); tailer.tail = null; // Çift bağlantılı liste, baş düğümün üst düğümü son düğüme işaret eder header.tail = tailer; // Kuyruk düğümünün alt düğümü baş düğüme işaret ediyor tailer.next = başlık; } public void put (K anahtarı, V değeri) { cacheMap.put (anahtar, değer); // Çift bağlantılı listeye bir düğüm ekleyin addNode (anahtar, değer); } public V get (K tuşu) { Düğüm < K, V > düğüm = getNode (anahtar); // Baş düğüme git moveToHead (düğüm); cacheMap.get (anahtar) döndür; } private void moveToHead (Düğüm < K, V > düğüm) { // Son düğüm ise eğer (node.tail == null) { node.next.tail = boş; tailer = node.next; nodeCount -; } // Başlangıçta baş düğüm ise, işlem yok eğer (node.next == null) { dönüş; } // Orta düğümde iseniz eğer (node.tail! = null node.next! = null) { // Önceki düğümü, geçerli düğümü de silen bir sonraki düğüme işaret eder node.tail.next = node.next; nodeCount -; } // Son olarak geçerli düğümü başa ekleyin // Burada bir nesneyi yenilemeniz gerektiğini unutmayın, aksi takdirde orijinal düğüm hala aşağıdaki referanslara sahiptir ve bu da bellek taşmasına neden olur. düğüm = yeni Düğüm < > (node.getKey (), node.getValue ()); addHead (düğüm); } / ** * Bağlantılı liste sorgusu verimsizdir * @param anahtarı * @dönüş * / özel Düğüm < K, V > getNode (K tuşu) { Düğüm < K, V > düğüm = tailer; while (düğüm! = boş) { eğer (node.getKey (). equals (key)) { dönüş düğümü; } düğüm = düğüm.next; } boş döndür; } / ** * Baş düğümü yaz * @param anahtarı * @param değeri * / private void addNode (K anahtarı, V değeri) { Düğüm < K, V > düğüm = yeni Düğüm < > (anahtar, değer); // Kapasite dolu ve sonuncuyu sil if (cacheSize == nodeCount) { // Son düğümü sil delTail (); } // Baş düğümü yaz addHead (düğüm); } / ** * Başlık düğümü ekle * * @param düğümü * / özel void addHead (Düğüm < K, V > düğüm) { // Baş düğümü yaz header.next = düğüm; node.tail = başlık; başlık = düğüm; nodeCount ++; // Yazılan veriler 2'den büyükse, başlatılan baş ve kuyruk düğümlerini silin eğer (nodeCount == 2) { tailer.next.next.tail = null; tailer = tailer.next.next; } } private void delTail () { // Son düğümü önbellekten silin cacheMap.remove (tailer.getKey ()); // Son düğümü sil tailer.next.tail = null; tailer = tailer.next; nodeCount--; } özel sınıf Düğümü < K, V > { özel K anahtarı; özel V değeri; Düğüm < K, V > kuyruk; Düğüm < K, V > Sonraki; public Düğüm (K anahtarı, V değeri) { this.key = anahtar; this.value = değer; } genel Düğüm () { } public K getKey () { Geri dönüş tuşu; } public void setKey (K tuşu) { this.key = anahtar; } public V getValue () { geri dönüş değeri; } public void setValue (V değeri) { this.value = değer; } } @Override public String toString () { StringBuilder sb = new StringBuilder (); Düğüm < K, V > düğüm = tailer; while (düğüm! = boş) { sb.append (node.getKey ()). append (":") .append (node.getValue ()) .append ("- > "); düğüm = düğüm.next; } return sb.toString (); } }Kaynak kodu:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUMap.java
Yazarken gerçek etki:
12 34 56 78 91011121314151617181920 @Ölçek public void put () Exception { LRUMap < Dize, Tamsayı > lruMap = yeni LRUMap (3); lruMap.put ("1"; 1); lruMap.put ("2"; 2); lruMap.put ("3"; 3); System.out.println (lruMap.toString ()); lruMap.put ("4"; 4); System.out.println (lruMap.toString ()); lruMap.put ("5"; 5); System.out.println (lruMap.toString ()); } //Çıktı: 1: 1 - > 2: 2-- > 3: 3-- > 2: 2-- > 3: 3-- > 4: 4-- > 3: 3-- > 4: 4-- > 5: 5-- >kullanırken:
@Ölçek public void get () Exception { LRUMap < Dize, Tamsayı > lruMap = yeni LRUMap (3); lruMap.put ("1"; 1); lruMap.put ("2"; 2); lruMap.put ("3"; 3); System.out.println (lruMap.toString ()); System.out.println ("=============="); Tamsayı tamsayı = lruMap.get ("1"); System.out.println (tamsayı); System.out.println ("=============="); System.out.println (lruMap.toString ()); } //Çıktı 1: 1 - > 2: 2-- > 3: 3-- > ============== 1 ============== 2: 2-- > 3: 3-- > 1: 1 - >Gerçekleşme fikri yukarıda bahsettiğimiz ile aynıdır, gelin anahtar noktalardan bahsedelim:
Aşağıda nesne ilişki diyagramı verilmiştir:
Başlatırken
Veri yazarken
12 LRUMap < Dize, Tamsayı > lruMap = yeni LRUMap (3); lruMap.put ("1"; 1); 1 lruMap.put ("2"; 2); 1 lruMap.put ("3"; 3); 1 lruMap.put ("4"; 4);Veri alırken
Veriler yukarıdakiyle aynıdır:
1 Tamsayı tamsayı = lruMap.get ("2");Verilerin nasıl saklandığının yukarıdaki resimlerle iyi anlaşılması gerekir.
Gerçekleşme üç
Aslında Java koleksiyonlarına aşina iseniz, yukarıdaki yapının LinkedHashMap'e çok benzediğini göreceksiniz.
Buna aşina olmayanlar önce LinkedHashMap'in temelindeki analizini anlayabilirler.
Böylece şunları başarmak için kullanabiliriz:
genel sınıf LRULinkedMap < K, V > { / ** * Maksimum önbellek boyutu * / özel int cacheSize; özel LinkedHashMap < K, V > cacheMap; public LRULinkedMap (int cacheSize) { this.cacheSize = cacheSize; cacheMap = yeni LinkedHashMap (16,0.75F, doğru) { @Override korumalı boole removeEldestEntry (Map.Entry eldest) { eğer (cacheSize + 1 == cacheMap.size ()) { doğruya dön; }Başka { yanlış dönüş; } } }; } public void put (K anahtarı, V değeri) { cacheMap.put (anahtar, değer); } public V get (K tuşu) { cacheMap.get (anahtar) döndür; } genel Koleksiyon < Harita Giriş < K, V > > getAll () { yeni DiziListesi döndür < Harita Giriş < K, V > > (cacheMap.entrySet ()); } }Kaynak kodu:
https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRULinkedMap.java
Bu sefer daha kısa, sadece birkaç satır kod (belirli bir mantık olan LinkedHashMap bizim için zaten uygulanmıştır)
gerçek etki:
@Ölçek public void put () Exception { LRULinkedMap < Dize, Tamsayı > map = new LRULinkedMap (3); map.put ("1", 1); map.put ("2"; 2); map.put ("3"; 3); için (Map.Entry < Dize, Tamsayı > e: map.getAll ()) { System.out.print (e.getKey () + ":" + e.getValue () + "\ t"); } System.out.println (""); map.put ("4", 4); için (Map.Entry < Dize, Tamsayı > e: map.getAll ()) { System.out.print (e.getKey () + ":" + e.getValue () + "\ t"); } } //Çıktı 1: 12: 23: 32: 23: 34: 4kullanırken:
@Ölçek public void get () Exception { LRULinkedMap < Dize, Tamsayı > map = new LRULinkedMap (4); map.put ("1", 1); map.put ("2"; 2); map.put ("3"; 3); map.put ("4", 4); için (Map.Entry < Dize, Tamsayı > e: map.getAll ()) { System.out.print (e.getKey () + ":" + e.getValue () + "\ t"); } System.out.println (""); map.get ("1"); için (Map.Entry < Dize, Tamsayı > e: map.getAll ()) { System.out.print (e.getKey () + ":" + e.getValue () + "\ t"); } } } //Çıktı 1: 12: 23: 34: 42: 23: 34: 41: 1LinkedHashMap ayrıca dahili olarak iki yönlü bir kuyruğu korur ve başlatma sırasında bir önbellek boyutu eşiği de verilir. Başlatma sırasında, son zamanlarda sık kullanılmayan verileri silmeniz gerekip gerekmediğini özelleştirebilirsiniz.Eğer öyleyse, verileri ikinci uygulamada olduğu gibi yönetecektir.
Aslında, ana kod LinkedHashMap'in removeEldestEntry yöntemini yeniden yazmaktır:
12 3 korumalı boole removeEldestEntry (Map.Entry < K, V > en yaşlı) { yanlış dönüş; }Varsayılan olarak yanlış döndürür, yani eşiğin aşılıp aşılmaması umursamaz.
Bu yüzden, LinkedHashMap'in en az son kullanılan verileri silmemize yardımcı olması için, eşikten büyük olduğunda doğruya dönecek şekilde özelleştiriyoruz.
sonuç olarak
Yukarıdaki, LRU önbelleğinin uygulanmasıdır ve neden en azından normal kullanımda olduğunu öğrenebilirsiniz.
Tabii ki guava uygulaması sektörde daha yaygın olarak kullanılıyor ve ayrıca çeşitli son kullanma stratejilerini de destekliyor.