Kuru mallar: bir LRU önbelleğinin uygulamalı uygulaması

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:

  • Bir LRU önbelleği uygulamak için, önbelleğe alınan veriler N'ye ulaştığında, en az kullanılan verilerin ortadan kaldırılması gerekir.
  • N saat içinde erişilemeyen verilerin de ortadan kaldırılması gerekir.

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:

  • HashMap ile aynı veri kaydetme yöntemini kullanır, ancak basitleştirilmiş bir sürümü manuel olarak uygular.
  • Her seferinde yazılan verileri kaydetmek için dahili olarak bir kuyruk kullanılır.
  • Yazma sırasında, ara belleğin eşik N'den daha büyük olup olmadığına karar verilir ve karşılanırsa, kuyruğun FIFO özelliklerine göre sıranın başındaki veriler silinir. Çünkü sıranın başındaki veriler ilk sırada yer almalıdır.
  • İlk olarak girilen verilerin güncel olup olmadığını belirlemek için bir arka plan programı iş parçacığı açılır (çünkü ilk yerleştirilen veriler bile büyük olasılıkla son kullanma koşulunu karşılar.)
  • Onu bir arka plan programı iş parçacığı olarak ayarlamak amacını daha iyi gösterebilir (en kötü durumda, bir kullanıcı iş parçacığı ise, sonunda programın normal şekilde çıkmamasına neden olabilir, çünkü iş parçacığı çalışmakta olduğundan, arka plan iş parçacığı bu duruma sahip olmayacaktır.)

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:

  • En son kullanılanı kaydetmek için, yazma sırasını sağlamak için en azından sıralı bir set gereklidir.
  • Veriler kullanıldıktan sonra sırası güncellenebilir.

Yukarıdaki iki noktaya dayanarak, yaygın olarak kullanılan bir veri yapısını düşünmek kolaydır: Bağlantılı liste .

  • Verileri, her veri yazıldığında bağlantılı listenin baş düğümüne yerleştirin.
  • Verileri kullanırken Baş düğümüne git .
  • Önbellek sayısı eşiği aştığında bağlantılı listenin kuyruk verilerini kaldırın.
  • 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:

    • Veriler, doğrudan HashMap kullanılarak saklanır.
    • Verileri depolamak için dahili olarak çift bağlantılı bir liste kullanılır, bu nedenle bir baş düğüm başlığı ve bir kuyruk düğüm noktası vardır.
    • Baş düğüme her yazışınızda ve kuyruk düğümünü sildiğinizde, bu başlık bilgisine bağlıdır.Eğer garip görünüyorsanız, bağlantılı bir listeyi kendiniz uygulamanız veya aşağıdaki nesne ilişkisi diyagramıyla birlikte anlamanız önerilir.
    • Bağlantılı listenin başına gitmek için verileri kullanırken, ilk adım düğümü çift bağlantılı listede bulmaktır. İşte bağlantılı listenin sorunu. Arama verimliliği çok düşüktür ve en kötüsü O (N) gerektirir. Ardından hareket etmek için mevcut düğüme güvenin.
    • Baş düğüm yazılırken, bağlantılı listenin boyutunun 2'ye eşit olduğuna karar verirken başlatılmış baş ve kuyruk düğümlerini silmek gerekir. Bunun nedeni, başlatma sırasında iki çift yönlü düğümün üretilmesi ve yalnızca bir veri yapısı oluşturacak veri olmamasıdır. Gerçek veriler geldiğinde, sonraki işlemleri kolaylaştırmak için silinmesi gerekir (bu nokta optimize edilmeye devam edebilir).
    • Yukarıdaki işlemlerin tümü iş parçacığı açısından güvenli değildir ve kullanıcı denetimi gerektirir.

    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: 4

    kullanı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: 1

    LinkedHashMap 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.

    Ma Guoming, Huang Xinying'in raydan çıkmasına, affetmeyi ve Huang Xinying'i tercih etmeyi seçti.
    önceki
    Kuru ürünler: Java Fork / Join çerçevesi
    Sonraki
    Denizdeki İpek Yolu, Birlikte Ulusal Esintiyi Değerleyen-Qingdao Shimao'nun Deniz Ulusal Esintisi Fuzhou Zekice Tadım Turu mükemmel bir şekilde sona eriyor
    Bir Yaprak Borsanın Yönünü Biliyor
    IQIYI'nin "Let's Run" Yedi Sezon Öldürme Bölümü Hafıza Öldürme, Yeni Kardeşlik Resmi Olarak Birleştirildi
    Fearless tarafından meydan okunan "Dünyanın 1 Numaralı Herkül" ün gerçek yüzü ortaya çıktı ve vücudu bir kule kadar güçlü.
    OnePlus 7 Pro'nun açılır selfie lensi olduğu doğrulandı
    "Senin ve benim gibi" dosyaları her Çarşamba öğlen saat 12'de yükseltiliyor Zhang Shaogang "güreş kalemi" Sha Yi'yi sorguluyor
    Qingdao'yu taşıyan, Shandong'un En İyi On İyi Kişisinden biri olarak seçildi.
    Mi Band 4 gerçek telefon önceden duyurulmuş, Lei Junun "R U OK" ifadesi tescilli ticari marka
    Edifier HECATE GM3 kulak içi Bluetooth oyun kulaklığı deneyimi, genç e-spor oyuncuları için bir zorunluluktur
    Adam köpek maması almak için sokağa gittiğinde, yakınlarda bir köpeğin ağladığını duydu ve onu aradı ve sıkıntılı olduğunu gördü.
    Güney Afrikalı astrofotograf, Satürn'ü Samsung S8 ve 8 inçlik Dobson teleskopuyla yakaladı
    Cai Shaofen'in 80 metrekarelik evinde 4 kişilik ailesi, konumu anladıktan sonra, netizenler buna parasının yetmeyeceğini söyledi!
    To Top