คุณจะใช้แคช LRU ใน Java ได้อย่างไร?


169

กรุณาอย่าพูด EHCache หรือ OSCache ฯลฯ สมมติว่ามีวัตถุประสงค์ของคำถามนี้ที่ฉันต้องการใช้งานของตัวเองโดยใช้เพียง SDK (การเรียนรู้ด้วยการทำ) ระบุว่าแคชจะใช้ในสภาพแวดล้อมแบบมัลติเธรดคุณจะใช้โครงสร้างข้อมูลแบบใด ฉันได้ติดตั้งแล้วโดยใช้LinkedHashMapและCollections # synchronMapแต่ฉันอยากรู้ว่าคอลเลกชันใหม่ที่เกิดขึ้นพร้อมกันนั้นจะเป็นตัวเลือกที่ดีกว่าหรือไม่

ปรับปรุง: ฉันเพิ่งอ่านล่าสุดของ Yeggeเมื่อฉันพบว่านักเก็ตนี้:

หากคุณต้องการการเข้าถึงอย่างต่อเนื่องและต้องการรักษาลำดับการแทรกคุณไม่สามารถทำได้ดีกว่า LinkedHashMap ซึ่งเป็นโครงสร้างข้อมูลที่ยอดเยี่ยมอย่างแท้จริง วิธีเดียวที่มันจะยอดเยี่ยมกว่านี้ก็คือถ้ามีเวอร์ชั่นที่เกิดขึ้นพร้อมกัน แต่อนิจจา

ฉันคิดว่าสิ่งเดียวกันเกือบเหมือนกันก่อนที่ฉันจะไปLinkedHashMap+ Collections#synchronizedMapใช้งานฉันกล่าวถึงข้างต้น ดีใจที่ได้ทราบฉันไม่ได้มองข้ามบางสิ่ง

จากคำตอบจนถึงตอนนี้ดูเหมือนว่าทางออกที่ดีที่สุดของฉันสำหรับ LRU ที่เกิดขึ้นพร้อมกันอย่างมากคือการขยายConcurrentHashMapโดยใช้ตรรกะเดียวกับที่LinkedHashMapใช้



คำถามที่คล้ายกันมากที่นี่
Mifeet

คำตอบ:


102

ผมชอบมาก ๆ ของคำแนะนำเหล่านี้ แต่ตอนนี้ผมคิดว่าผมจะติดกับ+LinkedHashMap Collections.synchronizedMapถ้าฉันไม่ทบทวนนี้ในอนาคตผมอาจจะทำงานในการขยายConcurrentHashMapในลักษณะเดียวกันขยายLinkedHashMapHashMap

UPDATE:

นี่คือส่วนสำคัญของการดำเนินงานปัจจุบันของฉันตามคำขอ

private class LruCache<A, B> extends LinkedHashMap<A, B> {
    private final int maxEntries;

    public LruCache(final int maxEntries) {
        super(maxEntries + 1, 1.0f, true);
        this.maxEntries = maxEntries;
    }

    /**
     * Returns <tt>true</tt> if this <code>LruCache</code> has more entries than the maximum specified when it was
     * created.
     *
     * <p>
     * This method <em>does not</em> modify the underlying <code>Map</code>; it relies on the implementation of
     * <code>LinkedHashMap</code> to do that, but that behavior is documented in the JavaDoc for
     * <code>LinkedHashMap</code>.
     * </p>
     *
     * @param eldest
     *            the <code>Entry</code> in question; this implementation doesn't care what it is, since the
     *            implementation is only dependent on the size of the cache
     * @return <tt>true</tt> if the oldest
     * @see java.util.LinkedHashMap#removeEldestEntry(Map.Entry)
     */
    @Override
    protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
        return super.size() > maxEntries;
    }
}

Map<String, String> example = Collections.synchronizedMap(new LruCache<String, String>(CACHE_SIZE));

15
อย่างไรก็ตามฉันต้องการใช้การห่อหุ้มที่นี่แทนการสืบทอด นี่คือสิ่งที่ฉันเรียนรู้จาก Effective Java
Kapil D

10
@KapilD ไม่นานมานี้ แต่ฉันเกือบจะเป็นบวกกับ JavaDocs ที่LinkedHashMapรับรองวิธีนี้อย่างชัดเจนในการสร้างการใช้งาน LRU
แฮงค์เกย์

7
@HankGay Java ของ LinkedHashMap (ที่มีพารามิเตอร์ที่สาม = จริง) ไม่ใช่แคช LRU นี่เป็นเพราะการใส่รายการใหม่ไม่ส่งผลกระทบต่อลำดับของรายการ (แคช LRU จริงจะวางรายการที่แทรกครั้งสุดท้ายที่ด้านหลังของคำสั่งวนซ้ำโดยไม่คำนึงว่ารายการนั้นมีอยู่ในแคชในครั้งแรกหรือไม่)
Pacerier

2
@Pierier ฉันไม่เห็นพฤติกรรมนี้เลย ด้วยแผนที่ที่เปิดใช้งาน accessOrder การกระทำทั้งหมดจะสร้างรายการตามที่ใช้ล่าสุด (ล่าสุด): การแทรกเริ่มต้นการอัพเดทค่าและการดึงค่า ฉันพลาดอะไรไปรึเปล่า?
Esailija

3
@Pacerier "การใส่รายการใหม่ไม่ส่งผลต่อลำดับของรายการ" สิ่งนี้ไม่ถูกต้อง หากคุณมองไปที่การใช้งานของ LinkedHashMap สำหรับวิธีการ "วาง" มันจะสืบทอดการใช้งานจาก HashMap Javadoc ของ HashMap กล่าวว่า "ถ้าแผนที่ก่อนหน้านี้มีการแมปสำหรับคีย์ค่าเก่าจะถูกแทนที่" และหากคุณตรวจสอบซอร์สโค้ดของมันเมื่อทำการแทนที่ค่าเก่ามันจะเรียกเมธอด recordAccess และในเมธอด recordAccess ของ LinkedHashMap มันจะมีลักษณะดังนี้: if (lm.accessOrder) {lm.modCount ++; ลบ (); addBefore (lm.header);}
nybon


10

นี่คือรอบที่สอง

รอบแรกเป็นสิ่งที่ฉันคิดขึ้นมาจากนั้นฉันอ่านความคิดเห็นกับโดเมนอีกครั้งในหัวของฉัน

ดังนั้นนี่คือรุ่นที่ง่ายที่สุดด้วยการทดสอบหน่วยที่แสดงว่าทำงานได้ตามรุ่นอื่น ๆ

รุ่นแรกที่ไม่เกิดขึ้นพร้อมกัน:

import java.util.LinkedHashMap;
import java.util.Map;

public class LruSimpleCache<K, V> implements LruCache <K, V>{

    Map<K, V> map = new LinkedHashMap (  );


    public LruSimpleCache (final int limit) {
           map = new LinkedHashMap <K, V> (16, 0.75f, true) {
               @Override
               protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) {
                   return super.size() > limit;
               }
           };
    }
    @Override
    public void put ( K key, V value ) {
        map.put ( key, value );
    }

    @Override
    public V get ( K key ) {
        return map.get(key);
    }

    //For testing only
    @Override
    public V getSilent ( K key ) {
        V value =  map.get ( key );
        if (value!=null) {
            map.remove ( key );
            map.put(key, value);
        }
        return value;
    }

    @Override
    public void remove ( K key ) {
        map.remove ( key );
    }

    @Override
    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }


}

ธงที่แท้จริงจะติดตามการเข้าถึงของได้รับและทำให้ ดู JavaDocs removeEdelstEntry ที่ไม่มีแฟล็กที่แท้จริงกับตัวสร้างจะใช้แคช FIFO (ดูหมายเหตุด้านล่างเกี่ยวกับ FIFO และ removeEldestEntry)

นี่คือการทดสอบที่พิสูจน์ว่าทำงานเป็นแคช LRU:

public class LruSimpleTest {

    @Test
    public void test () {
        LruCache <Integer, Integer> cache = new LruSimpleCache<> ( 4 );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

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


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );


        cache.put ( 4, 4 );
        cache.put ( 5, 5 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();


        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();


        if ( !ok ) die ();

    }

ตอนนี้สำหรับรุ่นที่เกิดขึ้นพร้อมกัน ...

แพคเกจ org.boon.cache;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LruSimpleConcurrentCache<K, V> implements LruCache<K, V> {

    final CacheMap<K, V>[] cacheRegions;


    private static class CacheMap<K, V> extends LinkedHashMap<K, V> {
        private final ReadWriteLock readWriteLock;
        private final int limit;

        CacheMap ( final int limit, boolean fair ) {
            super ( 16, 0.75f, true );
            this.limit = limit;
            readWriteLock = new ReentrantReadWriteLock ( fair );

        }

        protected boolean removeEldestEntry ( final Map.Entry<K, V> eldest ) {
            return super.size () > limit;
        }


        @Override
        public V put ( K key, V value ) {
            readWriteLock.writeLock ().lock ();

            V old;
            try {

                old = super.put ( key, value );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return old;

        }


        @Override
        public V get ( Object key ) {
            readWriteLock.writeLock ().lock ();
            V value;

            try {

                value = super.get ( key );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;
        }

        @Override
        public V remove ( Object key ) {

            readWriteLock.writeLock ().lock ();
            V value;

            try {

                value = super.remove ( key );
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;

        }

        public V getSilent ( K key ) {
            readWriteLock.writeLock ().lock ();

            V value;

            try {

                value = this.get ( key );
                if ( value != null ) {
                    this.remove ( key );
                    this.put ( key, value );
                }
            } finally {
                readWriteLock.writeLock ().unlock ();
            }
            return value;

        }

        public int size () {
            readWriteLock.readLock ().lock ();
            int size = -1;
            try {
                size = super.size ();
            } finally {
                readWriteLock.readLock ().unlock ();
            }
            return size;
        }

        public String toString () {
            readWriteLock.readLock ().lock ();
            String str;
            try {
                str = super.toString ();
            } finally {
                readWriteLock.readLock ().unlock ();
            }
            return str;
        }


    }

    public LruSimpleConcurrentCache ( final int limit, boolean fair ) {
        int cores = Runtime.getRuntime ().availableProcessors ();
        int stripeSize = cores < 2 ? 4 : cores * 2;
        cacheRegions = new CacheMap[ stripeSize ];
        for ( int index = 0; index < cacheRegions.length; index++ ) {
            cacheRegions[ index ] = new CacheMap<> ( limit / cacheRegions.length, fair );
        }
    }

    public LruSimpleConcurrentCache ( final int concurrency, final int limit, boolean fair ) {

        cacheRegions = new CacheMap[ concurrency ];
        for ( int index = 0; index < cacheRegions.length; index++ ) {
            cacheRegions[ index ] = new CacheMap<> ( limit / cacheRegions.length, fair );
        }
    }

    private int stripeIndex ( K key ) {
        int hashCode = key.hashCode () * 31;
        return hashCode % ( cacheRegions.length );
    }

    private CacheMap<K, V> map ( K key ) {
        return cacheRegions[ stripeIndex ( key ) ];
    }

    @Override
    public void put ( K key, V value ) {

        map ( key ).put ( key, value );
    }

    @Override
    public V get ( K key ) {
        return map ( key ).get ( key );
    }

    //For testing only
    @Override
    public V getSilent ( K key ) {
        return map ( key ).getSilent ( key );

    }

    @Override
    public void remove ( K key ) {
        map ( key ).remove ( key );
    }

    @Override
    public int size () {
        int size = 0;
        for ( CacheMap<K, V> cache : cacheRegions ) {
            size += cache.size ();
        }
        return size;
    }

    public String toString () {

        StringBuilder builder = new StringBuilder ();
        for ( CacheMap<K, V> cache : cacheRegions ) {
            builder.append ( cache.toString () ).append ( '\n' );
        }

        return builder.toString ();
    }


}

คุณสามารถดูว่าทำไมฉันถึงครอบคลุมรุ่นที่ไม่เกิดขึ้นพร้อมกันก่อน ความพยายามด้านบนเพื่อสร้างลายเส้นบางส่วนเพื่อลดความขัดแย้งในการล็อก ดังนั้นเราจึงซ่อนกุญแจและค้นหาแฮชนั้นเพื่อค้นหาแคชจริง สิ่งนี้ทำให้ขนาดข้อ จำกัด มากขึ้นสำหรับข้อเสนอแนะ / การคาดเดาคร่าวๆภายในข้อผิดพลาดจำนวนพอสมควรทั้งนี้ขึ้นอยู่กับว่าแฮชอัลกอริธึมแฮชของคีย์ของคุณกระจายกันอย่างไร

นี่คือการทดสอบเพื่อแสดงให้เห็นว่ารุ่นที่ทำงานพร้อมกันอาจใช้งานได้ :) (ทดสอบภายใต้ไฟจะเป็นวิธีจริง)

public class SimpleConcurrentLRUCache {


    @Test
    public void test () {
        LruCache <Integer, Integer> cache = new LruSimpleConcurrentCache<> ( 1, 4, false );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

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


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );


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

        puts (cache);
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();


        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();

        cache.put ( 8, 8 );
        cache.put ( 9, 9 );

        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();


        puts (cache);


        if ( !ok ) die ();

    }


    @Test
    public void test2 () {
        LruCache <Integer, Integer> cache = new LruSimpleConcurrentCache<> ( 400, false );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

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


        for (int index =0 ; index < 5_000; index++) {
            cache.get(0);
            cache.get ( 1 );
            cache.put ( 2, index  );
            cache.put ( 3, index );
            cache.put(index, index);
        }

        boolean ok = cache.getSilent ( 0 ) == 0 || die ();
        ok |= cache.getSilent ( 1 ) == 1 || die ();
        ok |= cache.getSilent ( 2 ) != null || die ();
        ok |= cache.getSilent ( 3 ) != null || die ();

        ok |= cache.size () < 600 || die();
        if ( !ok ) die ();



    }

}

นี่เป็นโพสต์สุดท้าย .. โพสต์แรกที่ฉันลบเนื่องจากเป็น LFU ไม่ใช่แคช LRU

ฉันคิดว่าฉันจะให้มันไปอีก ฉันพยายามพยายามหาแคชรุ่น LRU ที่ง่ายที่สุดโดยใช้มาตรฐาน JDK ที่ไม่มีการใช้งานมากเกินไป

นี่คือสิ่งที่ฉันมาด้วย ความพยายามครั้งแรกของฉันคือความหายนะเล็กน้อยเมื่อฉันติดตั้ง LFU และ LRU จากนั้นฉันก็เพิ่ม FIFO และ LRU ก็สนับสนุนมัน ... แล้วฉันก็รู้ว่ามันกลายเป็นสัตว์ประหลาด จากนั้นฉันก็เริ่มพูดคุยกับเพื่อนของฉันจอห์นผู้ที่แทบจะไม่สนใจแล้วฉันอธิบายในเชิงลึกว่าฉันใช้ LFU, LRU และ FIFO ได้อย่างไรและคุณจะสลับมันด้วย ARUM แบบง่าย ๆ ได้อย่างไรและจากนั้นฉันก็ตระหนักว่าทั้งหมดที่ฉันต้องการ เป็น LRU ง่าย ๆ ดังนั้นละเว้นโพสต์ก่อนหน้าจากฉันและแจ้งให้เราทราบหากคุณต้องการดูแคช LRU / LFU / FIFO ที่สามารถสลับได้ผ่าน enum ... ไม่? ตกลง .. ไปเลย

LRU ที่ง่ายที่สุดที่เป็นไปได้โดยใช้เพียง JDK ฉันใช้ทั้งรุ่นที่เกิดขึ้นพร้อมกันและรุ่นที่ไม่เกิดขึ้นพร้อมกัน

ฉันสร้างอินเทอร์เฟซทั่วไป (เป็นแบบมินิมอลดังนั้นอาจขาดคุณสมบัติบางอย่างที่คุณต้องการ แต่ใช้งานได้กับกรณีการใช้งานของฉัน แต่ถ้าคุณต้องการดูฟีเจอร์ XYZ แจ้งให้เราทราบ ... ฉันมีชีวิตอยู่เพื่อเขียนโค้ด) .

public interface LruCache<KEY, VALUE> {
    void put ( KEY key, VALUE value );

    VALUE get ( KEY key );

    VALUE getSilent ( KEY key );

    void remove ( KEY key );

    int size ();
}

คุณอาจสงสัยว่าgetSilentคืออะไร ฉันใช้สิ่งนี้เพื่อทดสอบ getSilent ไม่เปลี่ยนคะแนน LRU ของรายการ

ครั้งแรกที่ไม่เกิดขึ้นพร้อมกันหนึ่ง ....

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public class LruCacheNormal<KEY, VALUE> implements LruCache<KEY,VALUE> {

    Map<KEY, VALUE> map = new HashMap<> ();
    Deque<KEY> queue = new LinkedList<> ();
    final int limit;


    public LruCacheNormal ( int limit ) {
        this.limit = limit;
    }

    public void put ( KEY key, VALUE value ) {
        VALUE oldValue = map.put ( key, value );

        /*If there was already an object under this key,
         then remove it before adding to queue
         Frequently used keys will be at the top so the search could be fast.
         */
        if ( oldValue != null ) {
            queue.removeFirstOccurrence ( key );
        }
        queue.addFirst ( key );

        if ( map.size () > limit ) {
            final KEY removedKey = queue.removeLast ();
            map.remove ( removedKey );
        }

    }


    public VALUE get ( KEY key ) {

        /* Frequently used keys will be at the top so the search could be fast.*/
        queue.removeFirstOccurrence ( key );
        queue.addFirst ( key );
        return map.get ( key );
    }


    public VALUE getSilent ( KEY key ) {

        return map.get ( key );
    }

    public void remove ( KEY key ) {

        /* Frequently used keys will be at the top so the search could be fast.*/
        queue.removeFirstOccurrence ( key );
        map.remove ( key );
    }

    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }
}

queue.removeFirstOccurrence คือการดำเนินการอาจมีราคาแพงถ้าคุณมีแคชขนาดใหญ่ หนึ่งสามารถนำ LinkedList มาเป็นตัวอย่างและเพิ่มแฮชการค้นหาย้อนกลับจากองค์ประกอบหนึ่งไปอีกโหนดเพื่อทำการลบ A ล็อตที่เร็วขึ้นและสอดคล้องกันมากขึ้น ฉันเริ่มด้วย แต่แล้วฉันก็รู้ว่าฉันไม่ต้องการมัน แต่ ... อาจจะ ...

เมื่อใส่เรียกว่ากุญแจสำคัญในการได้รับการเพิ่มลงในคิว เมื่อได้รับการเรียกว่ากุญแจสำคัญในการได้รับการถอดออกและใหม่เพิ่มไปยังด้านบนของคิว

หากแคชของคุณมีขนาดเล็กและการสร้างรายการมีราคาแพงก็ควรเป็นแคชที่ดี หากแคชของคุณมีขนาดใหญ่จริงๆการค้นหาเชิงเส้นอาจเป็นคอขวดโดยเฉพาะถ้าคุณไม่มีพื้นที่แคชร้อน ยิ่งฮอตสปอตยิ่งเข้มขึ้นเท่าไหร่การค้นหาเชิงเส้นก็จะยิ่งรวดเร็วมากขึ้นเท่านั้นเมื่อไอเท็มยอดนิยมนั้นอยู่ด้านบนสุดของการค้นหาเชิงเส้นเสมอ อย่างไรก็ตาม ... สิ่งที่จำเป็นสำหรับการดำเนินการที่รวดเร็วกว่านี้คือการเขียน LinkedList อีกอันที่มีการดำเนินการลบที่มีองค์ประกอบย้อนกลับไปยังการค้นหาโหนดเพื่อลบจากนั้นการลบจะเร็วพอ ๆ กับการลบคีย์ออกจากแผนที่แฮช

หากคุณมีแคชต่ำกว่า 1,000 รายการสิ่งนี้น่าจะใช้ได้จริง

นี่คือการทดสอบง่ายๆเพื่อแสดงการทำงานของมัน

public class LruCacheTest {

    @Test
    public void test () {
        LruCache<Integer, Integer> cache = new LruCacheNormal<> ( 4 );


        cache.put ( 0, 0 );
        cache.put ( 1, 1 );

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


        boolean ok = cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 0 ) == 0 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();


        cache.put ( 4, 4 );
        cache.put ( 5, 5 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 0 ) == null || die ();
        ok |= cache.getSilent ( 1 ) == null || die ();
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == 4 || die ();
        ok |= cache.getSilent ( 5 ) == 5 || die ();

        if ( !ok ) die ();

    }
}

แคช LRU ที่ผ่านมาเป็นเธรดเดียวและโปรดอย่าห่อมันในสิ่งที่ทำข้อมูลให้ตรงกัน ....

นี่คือแทงที่รุ่นที่เกิดขึ้นพร้อมกัน

import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrentLruCache<KEY, VALUE> implements LruCache<KEY,VALUE> {

    private final ReentrantLock lock = new ReentrantLock ();


    private final Map<KEY, VALUE> map = new ConcurrentHashMap<> ();
    private final Deque<KEY> queue = new LinkedList<> ();
    private final int limit;


    public ConcurrentLruCache ( int limit ) {
        this.limit = limit;
    }

    @Override
    public void put ( KEY key, VALUE value ) {
        VALUE oldValue = map.put ( key, value );
        if ( oldValue != null ) {
            removeThenAddKey ( key );
        } else {
            addKey ( key );
        }
        if (map.size () > limit) {
            map.remove ( removeLast() );
        }
    }


    @Override
    public VALUE get ( KEY key ) {
        removeThenAddKey ( key );
        return map.get ( key );
    }


    private void addKey(KEY key) {
        lock.lock ();
        try {
            queue.addFirst ( key );
        } finally {
            lock.unlock ();
        }


    }

    private KEY removeLast( ) {
        lock.lock ();
        try {
            final KEY removedKey = queue.removeLast ();
            return removedKey;
        } finally {
            lock.unlock ();
        }
    }

    private void removeThenAddKey(KEY key) {
        lock.lock ();
        try {
            queue.removeFirstOccurrence ( key );
            queue.addFirst ( key );
        } finally {
            lock.unlock ();
        }

    }

    private void removeFirstOccurrence(KEY key) {
        lock.lock ();
        try {
            queue.removeFirstOccurrence ( key );
        } finally {
            lock.unlock ();
        }

    }


    @Override
    public VALUE getSilent ( KEY key ) {
        return map.get ( key );
    }

    @Override
    public void remove ( KEY key ) {
        removeFirstOccurrence ( key );
        map.remove ( key );
    }

    @Override
    public int size () {
        return map.size ();
    }

    public String toString () {
        return map.toString ();
    }
}

ความแตกต่างที่สำคัญคือการใช้ ConcurrentHashMap แทน HashMap และการใช้ Lock (ฉันอาจได้รับการซิงโครไนซ์ แต่ ... )

ฉันไม่ได้ทำการทดสอบภายใต้ไฟ แต่ดูเหมือนว่าแคช LRU แบบง่าย ๆ ที่อาจใช้งานได้ 80% ของกรณีการใช้งานที่คุณต้องการแผนที่ LRU แบบง่าย ๆ

ฉันยินดีรับข้อเสนอแนะยกเว้นทำไมคุณไม่ใช้ไลบรารี a, b หรือ c เหตุผลที่ฉันไม่ได้ใช้ห้องสมุดเสมอเพราะฉันไม่ต้องการให้ไฟล์สงครามทุกไฟล์เป็น 80MB และฉันเขียนไลบรารีดังนั้นฉันมักจะทำให้ libs plug-can พร้อมทางออกที่ดีพอและใครบางคนสามารถเสียบ - ในผู้ให้บริการแคชอื่นหากพวกเขาต้องการ :) ฉันไม่เคยรู้เลยว่าใครบางคนอาจต้องการ Guava หรือ ehcache หรืออย่างอื่นฉันไม่ต้องการรวมพวกเขา แต่ถ้าฉันทำการแคชปลั๊กอินฉันจะไม่ยกเว้นพวกเขาเช่นกัน

การลดการพึ่งพามีรางวัลเป็นของตัวเอง ฉันชอบที่จะได้รับคำติชมเกี่ยวกับวิธีทำให้เรื่องนี้ง่ายขึ้นหรือเร็วขึ้นหรือทั้งสองอย่าง

ถ้าใครรู้ว่าพร้อมที่จะไป ....

ตกลง .. ฉันรู้ว่าคุณกำลังคิดอะไรอยู่ ... ทำไมเขาไม่ใช้รายการ RemoveEldest จาก LinkedHashMap และดีที่ฉันควรจะ แต่ .... แต่ .. แต่ .. แต่ .. นั่นจะเป็น FIFO ไม่ใช่ LRU และพวกเรา พยายามใช้ LRU

    Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {

        @Override
        protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
            return this.size () > limit;
        }
    };

การทดสอบนี้ล้มเหลวสำหรับรหัสข้างต้น ...

        cache.get ( 2 );
        cache.get ( 3 );
        cache.put ( 6, 6 );
        cache.put ( 7, 7 );
        ok |= cache.size () == 4 || die ( "size" + cache.size () );
        ok |= cache.getSilent ( 2 ) == 2 || die ();
        ok |= cache.getSilent ( 3 ) == 3 || die ();
        ok |= cache.getSilent ( 4 ) == null || die ();
        ok |= cache.getSilent ( 5 ) == null || die ();

ดังนั้นนี่คือแคช FIFO ที่รวดเร็วและสกปรกโดยใช้ removeEldestEntry

import java.util.*;

public class FifoCache<KEY, VALUE> implements LruCache<KEY,VALUE> {

    final int limit;

    Map<KEY, VALUE> map = new LinkedHashMap<KEY, VALUE> () {

        @Override
        protected boolean removeEldestEntry ( Map.Entry<KEY, VALUE> eldest ) {
            return this.size () > limit;
        }
    };


    public LruCacheNormal ( int limit ) {
        this.limit = limit;
    }

    public void put ( KEY key, VALUE value ) {
         map.put ( key, value );


    }


    public VALUE get ( KEY key ) {

        return map.get ( key );
    }


    public VALUE getSilent ( KEY key ) {

        return map.get ( key );
    }

    public void remove ( KEY key ) {
        map.remove ( key );
    }

    public int size () {
        return map.size ();
    }

    public String toString() {
        return map.toString ();
    }
}

FIFO รวดเร็ว ไม่มีการค้นหารอบ ๆ คุณสามารถหน้า FIFO ต่อหน้า LRU และนั่นจะจัดการกับรายการที่ร้อนแรงที่สุด LRU ที่ดีขึ้นจะต้องใช้องค์ประกอบย้อนกลับไปยังคุณสมบัติของโหนด

อย่างไรก็ตาม ... ตอนนี้ฉันเขียนโค้ดให้ฉันอ่านคำตอบและดูว่าฉันพลาดอะไรไป ... ครั้งแรกที่ฉันสแกนมัน


9

LinkedHashMapคือ O (1) แต่ต้องการการซิงโครไนซ์ ไม่จำเป็นต้องบูรณาการล้อที่นั่น

2 ตัวเลือกสำหรับการเพิ่มภาวะพร้อมกัน:

1. สร้างหลายLinkedHashMapและกัญชาในพวกเขา: LinkedHashMap[4], index 0, 1, 2, 3ตัวอย่างเช่น: ที่สำคัญทำkey%4 (หรือbinary ORบน[key, 3]) เพื่อเลือกแผนที่ที่จะทำวาง / รับ / ลบ

2. คุณสามารถทำ LRU ที่ 'เกือบ' โดยการขยายConcurrentHashMapและมีแผนที่แฮชที่เชื่อมโยงเหมือนโครงสร้างในแต่ละภูมิภาคที่อยู่ภายใน การล็อคจะเกิดขึ้นอย่างละเอียดมากกว่าการLinkedHashMapซิงโครไนซ์ ในputหรือputIfAbsentเพียงล็อคหัวและหางของรายการที่จำเป็น (ต่อภูมิภาค) ในการลบหรือรับพื้นที่ทั้งหมดจะต้องถูกล็อค ฉันอยากรู้ว่ารายการที่เชื่อมโยงของอะตอมบางประเภทอาจช่วยได้ที่นี่ - อาจเป็นเช่นนั้นสำหรับหัวของรายการ อาจจะมากขึ้น

โครงสร้างจะไม่เก็บคำสั่งซื้อทั้งหมด แต่เพียงคำสั่งต่อภูมิภาค ตราบใดที่จำนวนของรายการนั้นใหญ่กว่าจำนวนภูมิภาคมากพอสำหรับแคชส่วนใหญ่ แต่ละภูมิภาคจะต้องมีการนับการเข้าของตนเองซึ่งจะถูกใช้แทนการนับทั่วโลกสำหรับทริกเกอร์การขับไล่ จำนวนภูมิภาคเริ่มต้นใน a ConcurrentHashMapคือ 16 ซึ่งเพียงพอสำหรับเซิร์ฟเวอร์ส่วนใหญ่ในปัจจุบัน

  1. จะง่ายต่อการเขียนและเร็วขึ้นภายใต้การทำงานพร้อมกันในระดับปานกลาง

  2. จะยากต่อการเขียน แต่ขยายขนาดได้ดีขึ้นพร้อมกันในระดับสูงมาก มันจะช้าลงสำหรับการเข้าถึงปกติ (เช่นเดียวกับConcurrentHashMapช้ากว่าHashMapที่ไม่มีการทำงานพร้อมกัน)


8

มีการใช้งานโอเพ่นซอร์สสองแบบ

Apache Solr มี ConcurrentLRUCache: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcurrentLRUCache.html

มีโครงการโอเพ่นซอร์สสำหรับ ConcurrentLinkedHashMap: http://code.google.com/p/concurrentlinkedhashmap/


2
ทางออกของ Solr ไม่ใช่ LRU จริง ๆ แต่ConcurrentLinkedHashMapน่าสนใจ มันอ้างว่าได้รับการรีดMapMakerจาก Guava แต่ฉันไม่ได้เห็นมันในเอกสาร ความคิดใดที่เกิดขึ้นกับความพยายามนั้น
แฮงค์เกย์

3
เวอร์ชันที่เรียบง่ายถูกรวมเข้าด้วยกัน แต่การทดสอบยังไม่เสร็จสมบูรณ์ดังนั้นจึงยังไม่เป็นแบบสาธารณะ ฉันมีปัญหามากมายในการผสานรวมที่ลึกขึ้น แต่ฉันหวังว่าจะทำให้เสร็จเพราะมีคุณสมบัติอัลกอริทึมที่ดี ความสามารถในการฟังการขับไล่ (ความจุการหมดอายุ GC) ถูกเพิ่มและขึ้นอยู่กับวิธีการของ CLHM (คิวฟัง) ฉันยังต้องการมีส่วนร่วมในความคิดของ "ค่าถ่วงน้ำหนัก" เช่นกันเนื่องจากเป็นประโยชน์เมื่อแคชคอลเลกชัน น่าเสียดายเนื่องจากข้อผูกพันอื่น ๆ ฉันได้รับล้นมือเกินกว่าที่จะอุทิศเวลาที่ Guava สมควรได้รับ (และฉันสัญญากับ Kevin / Charles)
Ben Manes

3
อัปเดต: การรวมเสร็จสมบูรณ์และเผยแพร่ใน Guava r08 สิ่งนี้ผ่านการตั้งค่า #maximumSize ()
Ben Manes

7

ฉันจะพิจารณาใช้java.util.concurrent.PriorityBlockingQueueโดยลำดับความสำคัญถูกกำหนดโดยตัวนับ "numberOfUses" ในแต่ละองค์ประกอบ ฉันจะระมัดระวังในการทำให้การซิงโครไนซ์ของฉันถูกต้องทั้งหมดเนื่องจากตัวนับ "numberOfUses" บอกเป็นนัยว่าองค์ประกอบไม่สามารถเปลี่ยนแปลงได้

วัตถุองค์ประกอบจะเป็น wrapper สำหรับวัตถุในแคช:

class CacheElement {
    private final Object obj;
    private int numberOfUsers = 0;

    CacheElement(Object obj) {
        this.obj = obj;
    }

    ... etc.
}

คุณไม่ได้หมายความว่าจะต้องไม่เปลี่ยนรูป?
shsteimer

2
โปรดทราบว่าถ้าคุณพยายามที่จะทำลำดับความสำคัญบล็อกเนื้อหาที่กล่าวถึงโดย steve mcleod คุณควรทำให้องค์ประกอบไม่เปลี่ยนรูปเนื่องจากการแก้ไของค์ประกอบในคิวจะไม่มีผลกระทบคุณจะต้องลบองค์ประกอบและเพิ่มใหม่อีกครั้งเพื่อ จัดลำดับความสำคัญใหม่อีกครั้ง
James

เจมส์ด้านล่างชี้ข้อผิดพลาดที่ฉันทำ ซึ่งฉันเสนอเป็นหลักฐานว่าเลือดไหลยากแค่ไหนที่จะเขียนแคชที่เชื่อถือได้และทนทาน
Steve McLeod

6

หวังว่านี่จะช่วยได้

import java.util.*;
public class Lru {

public static <K,V> Map<K,V> lruCache(final int maxSize) {
    return new LinkedHashMap<K, V>(maxSize*4/3, 0.75f, true) {

        private static final long serialVersionUID = -3588047435434569014L;

        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxSize;
        }
    };
 }
 public static void main(String[] args ) {
    Map<Object, Object> lru = Lru.lruCache(2);      
    lru.put("1", "1");
    lru.put("2", "2");
    lru.put("3", "3");
    System.out.println(lru);
}
}

1
เป็นตัวอย่างที่ดี! คุณสามารถแสดงความคิดเห็นว่าทำไมต้องตั้งความจุ maxSize * 4/3?
Akvel

1
@Avel มันเรียกว่าความจุเริ่มต้นสามารถเป็นค่าใด ๆ [จำนวนเต็ม] ในขณะที่ 0.75f ​​เป็นค่าตัวประกอบการโหลดเริ่มต้นหวังว่าลิงก์นี้จะช่วย: ashishsharma.me/2011/09/custom-lru-cache-java.html
murating

5

แคช LRU สามารถใช้งานได้โดยใช้ ConcurrentLinkedQueue และ ConcurrentHashMap ซึ่งสามารถใช้ในสถานการณ์จำลองมัลติเธรดได้เช่นกัน ส่วนหัวของคิวคือองค์ประกอบที่อยู่ในคิวนานที่สุด ส่วนท้ายของคิวคือองค์ประกอบที่อยู่ในคิวในเวลาที่สั้นที่สุด เมื่อองค์ประกอบมีอยู่ในแผนที่เราสามารถลบออกจาก LinkedQueue และแทรกที่ส่วนท้าย

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

public class LRUCache<K,V> {
  private ConcurrentHashMap<K,V> map;
  private ConcurrentLinkedQueue<K> queue;
  private final int size; 

  public LRUCache(int size) {
    this.size = size;
    map = new ConcurrentHashMap<K,V>(size);
    queue = new ConcurrentLinkedQueue<K>();
  }

  public V get(K key) {
    //Recently accessed, hence move it to the tail
    queue.remove(key);
    queue.add(key);
    return map.get(key);
  }

  public void put(K key, V value) {
    //ConcurrentHashMap doesn't allow null key or values
    if(key == null || value == null) throw new NullPointerException();
    if(map.containsKey(key) {
      queue.remove(key);
    }
    if(queue.size() >= size) {
      K lruKey = queue.poll();
      if(lruKey != null) {
        map.remove(lruKey);
      }
    }
    queue.add(key);
    map.put(key,value);
  }

}

นี่ไม่ใช่ threadsafe ตัวอย่างเช่นคุณสามารถได้อย่างง่ายดายเกินขนาด LRU putสูงสุดโดยพร้อมกันเรียก
dpeacock

โปรดแก้ไขให้ถูกต้อง ก่อนอื่นมันไม่ได้รวบรวมใน line map.containKey (กุญแจ) ประการที่สองใน get () คุณควรตรวจสอบว่าคีย์ถูกลบออกจริงๆไม่เช่นนั้นแผนที่และคิวจะไม่ซิงค์และ "queue.size ()> = size" จะกลายเป็นจริงเสมอ ฉันจะโพสต์เวอร์ชันของฉันโดยแก้ไขปัญหานี้ตามที่ฉันชอบความคิดของคุณในการใช้คอลเลกชันทั้งสองนี้
Aleksander Lech

3

นี่คือการดำเนินการของฉันสำหรับ LRU ฉันใช้ PriorityQueue ซึ่งโดยทั่วไปจะทำงานเป็น FIFO และไม่ใช่ threadsafe Used Comparator ขึ้นอยู่กับการสร้างเวลาหน้าและอ้างอิงการเรียงลำดับของหน้าเว็บสำหรับเวลาที่ใช้น้อยที่สุดเมื่อเร็ว ๆ นี้

หน้าเพื่อพิจารณา: 2, 1, 0, 2, 8, 2, 4

หน้าเพิ่มลงในแคชคือ: 2
หน้าเพิ่มลงในแคชคือ: 1
หน้าเพิ่มลงในแคชคือ: 0
หน้า: 2 มีอยู่แล้วในแคช เวลาที่เข้าถึงล่าสุดที่อัปเดตแล้ว
Page Fault, PAGE: 1, แทนที่ด้วย PAGE: 8
หน้าถูกเพิ่มในแคชคือ: 8
หน้า: 2 มีอยู่แล้วในแคช เวลาที่เข้าถึงล่าสุดที่อัปเดตแล้ว
Page Fault, PAGE: 0, แทนที่ด้วย PAGE: 4
หน้าถูกเพิ่มในแคชคือ: 4

เอาท์พุท

LRUCache Pages
------------- PageName
: 8, PageCreationTime: 1365957019974 PageName
: 2, PageCreationTime: 1365957020074 PageName
: 4, PageCreationTime: 1365957020174

ใส่รหัสที่นี่

import java.util.Comparator;
import java.util.Iterator;
import java.util.PriorityQueue;


public class LRUForCache {
    private PriorityQueue<LRUPage> priorityQueue = new PriorityQueue<LRUPage>(3, new LRUPageComparator());
    public static void main(String[] args) throws InterruptedException {

        System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4");
        System.out.println("----------------------------------------------\n");

        LRUForCache cache = new LRUForCache();
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("1"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("0"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("8"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("2"));
        Thread.sleep(100);
        cache.addPageToQueue(new LRUPage("4"));
        Thread.sleep(100);

        System.out.println("\nLRUCache Pages");
        System.out.println("-------------");
        cache.displayPriorityQueue();
    }


    public synchronized void  addPageToQueue(LRUPage page){
        boolean pageExists = false;
        if(priorityQueue.size() == 3){
            Iterator<LRUPage> iterator = priorityQueue.iterator();

            while(iterator.hasNext()){
                LRUPage next = iterator.next();
                if(next.getPageName().equals(page.getPageName())){
                    /* wanted to just change the time, so that no need to poll and add again.
                       but elements ordering does not happen, it happens only at the time of adding
                       to the queue

                       In case somebody finds it, plz let me know.
                     */
                    //next.setPageCreationTime(page.getPageCreationTime()); 

                    priorityQueue.remove(next);
                    System.out.println("Page: " + page.getPageName() + " already exisit in cache. Last accessed time updated");
                    pageExists = true;
                    break;
                }
            }
            if(!pageExists){
                // enable it for printing the queue elemnts
                //System.out.println(priorityQueue);
                LRUPage poll = priorityQueue.poll();
                System.out.println("Page Fault, PAGE: " + poll.getPageName()+", Replaced with PAGE: "+page.getPageName());

            }
        }
        if(!pageExists){
            System.out.println("Page added into cache is : " + page.getPageName());
        }
        priorityQueue.add(page);

    }

    public void displayPriorityQueue(){
        Iterator<LRUPage> iterator = priorityQueue.iterator();
        while(iterator.hasNext()){
            LRUPage next = iterator.next();
            System.out.println(next);
        }
    }
}

class LRUPage{
    private String pageName;
    private long pageCreationTime;
    public LRUPage(String pagename){
        this.pageName = pagename;
        this.pageCreationTime = System.currentTimeMillis();
    }

    public String getPageName() {
        return pageName;
    }

    public long getPageCreationTime() {
        return pageCreationTime;
    }

    public void setPageCreationTime(long pageCreationTime) {
        this.pageCreationTime = pageCreationTime;
    }

    @Override
    public boolean equals(Object obj) {
        LRUPage page = (LRUPage)obj; 
        if(pageCreationTime == page.pageCreationTime){
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (int) (31 * pageCreationTime);
    }

    @Override
    public String toString() {
        return "PageName: " + pageName +", PageCreationTime: "+pageCreationTime;
    }
}


class LRUPageComparator implements Comparator<LRUPage>{

    @Override
    public int compare(LRUPage o1, LRUPage o2) {
        if(o1.getPageCreationTime() > o2.getPageCreationTime()){
            return 1;
        }
        if(o1.getPageCreationTime() < o2.getPageCreationTime()){
            return -1;
        }
        return 0;
    }
}

2

นี่คือการทดสอบการใช้แคช LRU ที่ทำงานได้ดีที่สุดในเวลาเดียวกันโดยไม่มีบล็อกที่ตรงกัน

public class ConcurrentLRUCache<Key, Value> {

private final int maxSize;

private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;

public ConcurrentLRUCache(final int maxSize) {
    this.maxSize = maxSize;
    map = new ConcurrentHashMap<Key, Value>(maxSize);
    queue = new ConcurrentLinkedQueue<Key>();
}

/**
 * @param key - may not be null!
 * @param value - may not be null!
 */
public void put(final Key key, final Value value) {
    if (map.containsKey(key)) {
        queue.remove(key); // remove the key from the FIFO queue
    }

    while (queue.size() >= maxSize) {
        Key oldestKey = queue.poll();
        if (null != oldestKey) {
            map.remove(oldestKey);
        }
    }
    queue.add(key);
    map.put(key, value);
}

/**
 * @param key - may not be null!
 * @return the value associated to the given key or null
 */
public Value get(final Key key) {
    return map.get(key);
}

}


1
@zoltan boda .... คุณไม่ได้จัดการกับสถานการณ์เดียว .. จะเกิดอะไรขึ้นถ้าวัตถุเดียวกันถูกใช้หลายครั้ง ในกรณีนี้เราไม่ควรเพิ่มหลายรายการสำหรับวัตถุเดียวกัน ... แทนที่จะเป็นกุญแจสำคัญ

5
คำเตือน: นี่ไม่ใช่แคช LRU ในแคช LRU คุณทิ้งรายการที่เข้าถึงน้อยที่สุด อันนี้โยนรายการที่น้อยที่สุดที่เพิ่งเขียน นอกจากนี้ยังเป็นการสแกนเชิงเส้นเพื่อดำเนินการกับคิวลบ (คีย์)
Dave L.

ConcurrentLinkedQueue # size () ไม่ใช่การใช้เวลาคงที่
NateS

3
วิธีการใส่ของคุณดูไม่ปลอดภัย - มีคำสั่งตรวจสอบจากนั้นดำเนินการบางรายการที่จะแบ่งเป็นหลายเธรด
assylias

2

นี่คือแคช LRU ที่ฉันใช้ซึ่งจะห่อหุ้ม LinkedHashMap และจัดการการทำงานพร้อมกันด้วยการล็อคแบบซิงโครไนซ์อย่างง่ายเพื่อป้องกันจุดที่ฉ่ำ มัน "สัมผัส" องค์ประกอบตามที่พวกเขาใช้เพื่อให้พวกเขากลายเป็นองค์ประกอบ "สด" อีกครั้งเพื่อให้เป็นจริง LRU ฉันยังมีข้อกำหนดขององค์ประกอบของฉันที่มีอายุการใช้งานขั้นต่ำซึ่งคุณสามารถคิดได้ว่าเป็น "เวลาว่างสูงสุด" ที่ได้รับอนุญาตจากนั้นคุณพร้อมที่จะขับไล่

แต่ผมเห็นด้วยกับข้อสรุปของแฮงค์และคำตอบที่ได้รับการยอมรับ - CacheBuilderถ้าฉันได้เริ่มต้นอีกครั้งในวันนี้ผมจะตรวจสอบของฝรั่ง

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


public class MaxIdleLRUCache<KK, VV> {

    final static private int IDEAL_MAX_CACHE_ENTRIES = 128;

    public interface DeadElementCallback<KK, VV> {
        public void notify(KK key, VV element);
    }

    private Object lock = new Object();
    private long minAge;
    private HashMap<KK, Item<VV>> cache;


    public MaxIdleLRUCache(long minAgeMilliseconds) {
        this(minAgeMilliseconds, IDEAL_MAX_CACHE_ENTRIES);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries) {
        this(minAgeMilliseconds, idealMaxCacheEntries, null);
    }

    public MaxIdleLRUCache(long minAgeMilliseconds, int idealMaxCacheEntries, final DeadElementCallback<KK, VV> callback) {
        this.minAge = minAgeMilliseconds;
        this.cache = new LinkedHashMap<KK, Item<VV>>(IDEAL_MAX_CACHE_ENTRIES + 1, .75F, true) {
            private static final long serialVersionUID = 1L;

            // This method is called just after a new entry has been added
            public boolean removeEldestEntry(Map.Entry<KK, Item<VV>> eldest) {
                // let's see if the oldest entry is old enough to be deleted. We don't actually care about the cache size.
                long age = System.currentTimeMillis() - eldest.getValue().birth;
                if (age > MaxIdleLRUCache.this.minAge) {
                    if ( callback != null ) {
                        callback.notify(eldest.getKey(), eldest.getValue().payload);
                    }
                    return true; // remove it
                }
                return false; // don't remove this element
            }
        };

    }

    public void put(KK key, VV value) {
        synchronized ( lock ) {
//          System.out.println("put->"+key+","+value);
            cache.put(key, new Item<VV>(value));
        }
    }

    public VV get(KK key) {
        synchronized ( lock ) {
//          System.out.println("get->"+key);
            Item<VV> item = getItem(key);
            return item == null ? null : item.payload;
        }
    }

    public VV remove(String key) {
        synchronized ( lock ) {
//          System.out.println("remove->"+key);
            Item<VV> item =  cache.remove(key);
            if ( item != null ) {
                return item.payload;
            } else {
                return null;
            }
        }
    }

    public int size() {
        synchronized ( lock ) {
            return cache.size();
        }
    }

    private Item<VV> getItem(KK key) {
        Item<VV> item = cache.get(key);
        if (item == null) {
            return null;
        }
        item.touch(); // idle the item to reset the timeout threshold
        return item;
    }

    private static class Item<T> {
        long birth;
        T payload;

        Item(T payload) {
            this.birth = System.currentTimeMillis();
            this.payload = payload;
        }

        public void touch() {
            this.birth = System.currentTimeMillis();
        }
    }

}

2

สำหรับแคชโดยทั่วไปคุณจะต้องค้นหาข้อมูลบางอย่างผ่านทางวัตถุพร็อกซี (URL, สตริง .... ) ดังนั้นอินเทอร์เฟซที่ชาญฉลาดคุณจะต้องการแผนที่ แต่หากต้องการกำจัดสิ่งต่าง ๆ ออกไปคุณต้องการคิวแบบโครงสร้าง ภายในฉันจะรักษาโครงสร้างข้อมูลไว้สองรายการคือ Priority-Queue และ HashMap นี่คือการนำไปปฏิบัติที่ควรจะทำทุกอย่างในเวลา O (1)

นี่คือคลาสที่ฉันทำวิปอย่างรวดเร็ว:

import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V>
{
    int maxSize;
    int currentSize = 0;

    Map<K, ValueHolder<K, V>> map;
    LinkedList<K> queue;

    public LRUCache(int maxSize)
    {
        this.maxSize = maxSize;
        map = new HashMap<K, ValueHolder<K, V>>();
        queue = new LinkedList<K>();
    }

    private void freeSpace()
    {
        K k = queue.remove();
        map.remove(k);
        currentSize--;
    }

    public void put(K key, V val)
    {
        while(currentSize >= maxSize)
        {
            freeSpace();
        }
        if(map.containsKey(key))
        {//just heat up that item
            get(key);
            return;
        }
        ListNode<K> ln = queue.add(key);
        ValueHolder<K, V> rv = new ValueHolder<K, V>(val, ln);
        map.put(key, rv);       
        currentSize++;
    }

    public V get(K key)
    {
        ValueHolder<K, V> rv = map.get(key);
        if(rv == null) return null;
        queue.remove(rv.queueLocation);
        rv.queueLocation = queue.add(key);//this ensures that each item has only one copy of the key in the queue
        return rv.value;
    }
}

class ListNode<K>
{
    ListNode<K> prev;
    ListNode<K> next;
    K value;
    public ListNode(K v)
    {
        value = v;
        prev = null;
        next = null;
    }
}

class ValueHolder<K,V>
{
    V value;
    ListNode<K> queueLocation;
    public ValueHolder(V value, ListNode<K> ql)
    {
        this.value = value;
        this.queueLocation = ql;
    }
}

class LinkedList<K>
{
    ListNode<K> head = null;
    ListNode<K> tail = null;

    public ListNode<K> add(K v)
    {
        if(head == null)
        {
            assert(tail == null);
            head = tail = new ListNode<K>(v);
        }
        else
        {
            tail.next = new ListNode<K>(v);
            tail.next.prev = tail;
            tail = tail.next;
            if(tail.prev == null)
            {
                tail.prev = head;
                head.next = tail;
            }
        }
        return tail;
    }

    public K remove()
    {
        if(head == null)
            return null;
        K val = head.value;
        if(head.next == null)
        {
            head = null;
            tail = null;
        }
        else
        {
            head = head.next;
            head.prev = null;
        }
        return val;
    }

    public void remove(ListNode<K> ln)
    {
        ListNode<K> prev = ln.prev;
        ListNode<K> next = ln.next;
        if(prev == null)
        {
            head = next;
        }
        else
        {
            prev.next = next;
        }
        if(next == null)
        {
            tail = prev;
        }
        else
        {
            next.prev = prev;
        }       
    }
}

นี่คือวิธีการทำงาน คีย์จะถูกเก็บไว้ในรายการที่เชื่อมโยงกับคีย์ที่เก่าที่สุดที่อยู่ด้านหน้าของรายการ (ปุ่มใหม่ไปทางด้านหลัง) ดังนั้นเมื่อคุณต้องการ 'นำ' สิ่งที่คุณเพิ่งโผล่ออกมาด้านหน้าของคิวแล้วใช้คีย์เพื่อ ลบค่าออกจากแผนที่ เมื่อรายการได้รับการอ้างอิงคุณคว้า ValueHolder จากแผนที่จากนั้นใช้ตัวแปร Queuelocation เพื่อลบคีย์ออกจากตำแหน่งปัจจุบันในคิวจากนั้นวางไว้ที่ด้านหลังของคิว (ตอนนี้ใช้ล่าสุด) การเพิ่มสิ่งต่าง ๆ ค่อนข้างเหมือนกัน

ฉันแน่ใจว่ามีข้อผิดพลาดมากมายที่นี่และฉันไม่ได้ใช้การซิงโครไนซ์ แต่คลาสนี้จะให้ O (1) การเพิ่มแคช, O (1) การลบรายการเก่าและการดึงรายการแคช O (1) แม้แต่การซิงโครไนซ์เล็กน้อย (เพียงซิงโครไนซ์ทุกวิธีสาธารณะ) จะยังคงมีการช่วงชิงล็อกเล็กน้อยเนื่องจากเวลาทำงาน หากใครมีเทคนิคการซิงโครไนซ์ที่ชาญฉลาดฉันจะสนใจ นอกจากนี้ฉันแน่ใจว่ามีการเพิ่มประสิทธิภาพเพิ่มเติมบางอย่างที่คุณสามารถนำไปใช้โดยใช้ตัวแปร maxsize เกี่ยวกับแผนที่


ขอบคุณสำหรับระดับของรายละเอียด แต่สิ่งนี้มอบชัยชนะเหนือการนำไปใช้LinkedHashMap+ Collections.synchronizedMap()อย่างไร
แฮงค์เกย์

ประสิทธิภาพฉันไม่รู้แน่ชัด แต่ฉันไม่คิดว่า LinkedHashMap มีการแทรก O (1) (อาจเป็น O (log (n))) จริง ๆ แล้วคุณสามารถเพิ่มวิธีการสองสามอย่างเพื่อทำให้ส่วนต่อประสานแผนที่ในการใช้งานของฉันสมบูรณ์ จากนั้นใช้ Collections.synchronizedMap เพื่อเพิ่มการทำงานพร้อมกัน
ลูกา

ในคลาส LinkedList ด้านบนในวิธีการเพิ่มมีรหัสในบล็อกอื่นคือ if (tail.prev == null) {tail.prev = head; head.next = tail; } เมื่อรหัสนี้จะถูกดำเนินการ? ฉันวิ่งไม่กี่ครั้งและฉันคิดว่าสิ่งนี้จะไม่ถูกดำเนินการและควรลบออก
Dipesh

1

มีลักษณะที่ConcurrentSkipListMap ควรให้เวลาเข้าสู่ระบบ (n) สำหรับการทดสอบและลบองค์ประกอบหากมีอยู่แล้วในแคชและเวลาคงที่สำหรับการเพิ่มอีกครั้ง

คุณเพียงแค่ต้องการตัวนับจำนวนหนึ่งและองค์ประกอบตัวห่อหุ้มเพื่อบังคับให้มีการสั่งซื้อคำสั่ง LRU และให้แน่ใจว่าข้อมูลล่าสุดจะถูกลบทิ้งเมื่อแคชเต็ม


จะConcurrentSkipListMapให้ประโยชน์ในการใช้งานที่ง่ายกว่าConcurrentHashMapหรือเป็นกรณีของการหลีกเลี่ยงกรณีทางพยาธิวิทยาหรือไม่?
แฮงค์เกย์

มันจะทำให้สิ่งต่าง ๆ ง่ายขึ้นเนื่องจากองค์ประกอบการสั่งซื้อของ ConcurrentSkipListMap ซึ่งจะช่วยให้คุณสามารถจัดการสิ่งที่ใช้ในการสั่งซื้อ ConcurrentHashMap ไม่ได้ทำสิ่งนี้ดังนั้นโดยทั่วไปคุณจะต้องทำซ้ำเนื้อหาแคชทั้งหมดเพื่ออัปเดตองค์ประกอบล่าสุด ใช้เคาน์เตอร์ 'หรืออะไรก็ตาม
madlep

ดังนั้นด้วยConcurrentSkipListMapการนำไปใช้ฉันจะสร้างการใช้งานใหม่ของMapอินเทอร์เฟซที่ได้รับมอบหมายConcurrentSkipListMapและทำการเรียงลำดับบางอย่างเพื่อให้มีการห่อหุ้มคีย์ชนิดใดก็ได้ที่เรียงลำดับตามการเข้าถึงล่าสุดได้อย่างง่ายดายหรือไม่
แฮงค์เกย์

1

นี่คือการใช้งานสั้น ๆ ของฉันโปรดวิพากษ์วิจารณ์หรือปรับปรุง!

package util.collection;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Limited size concurrent cache map implementation.<br/>
 * LRU: Least Recently Used.<br/>
 * If you add a new key-value pair to this cache after the maximum size has been exceeded,
 * the oldest key-value pair will be removed before adding.
 */

public class ConcurrentLRUCache<Key, Value> {

private final int maxSize;
private int currentSize = 0;

private ConcurrentHashMap<Key, Value> map;
private ConcurrentLinkedQueue<Key> queue;

public ConcurrentLRUCache(final int maxSize) {
    this.maxSize = maxSize;
    map = new ConcurrentHashMap<Key, Value>(maxSize);
    queue = new ConcurrentLinkedQueue<Key>();
}

private synchronized void freeSpace() {
    Key key = queue.poll();
    if (null != key) {
        map.remove(key);
        currentSize = map.size();
    }
}

public void put(Key key, Value val) {
    if (map.containsKey(key)) {// just heat up that item
        put(key, val);
        return;
    }
    while (currentSize >= maxSize) {
        freeSpace();
    }
    synchronized(this) {
        queue.add(key);
        map.put(key, val);
        currentSize++;
    }
}

public Value get(Key key) {
    return map.get(key);
}
}

1
นี่ไม่ใช่แคช LRU เพียงแค่เป็นแคช FIFO
lslab

1

นี่คือการดำเนินการของฉันกับปัญหานี้

Simplelrucache ให้การแคช LRU ของ threadsafe ที่ง่ายและไม่กระจายด้วยการรองรับ TTL มันมีสองการใช้งาน:

  • พร้อมกันขึ้นอยู่กับ ConcurrentLinkedHashMap
  • ทำข้อมูลให้ตรงกันตาม LinkedHashMap

คุณสามารถค้นหาได้ที่นี่: http://code.google.com/p/simplelrucache/


1

วิธีที่ดีที่สุดในการบรรลุคือการใช้ LinkedHashMap ที่รักษาลำดับการแทรกขององค์ประกอบ ต่อไปนี้เป็นตัวอย่างรหัส:

public class Solution {

Map<Integer,Integer> cache;
int capacity;
public Solution(int capacity) {
    this.cache = new LinkedHashMap<Integer,Integer>(capacity); 
    this.capacity = capacity;

}

// This function returns false if key is not 
// present in cache. Else it moves the key to 
// front by first removing it and then adding 
// it, and returns true. 

public int get(int key) {
if (!cache.containsKey(key)) 
        return -1; 
    int value = cache.get(key);
    cache.remove(key); 
    cache.put(key,value); 
    return cache.get(key); 

}

public void set(int key, int value) {

    // If already present, then  
    // remove it first we are going to add later 
       if(cache.containsKey(key)){
        cache.remove(key);
    }
     // If cache size is full, remove the least 
    // recently used. 
    else if (cache.size() == capacity) { 
        Iterator<Integer> iterator = cache.keySet().iterator();
        cache.remove(iterator.next()); 
    }
        cache.put(key,value);
}

}


0

ฉันกำลังมองหาแคช LRU ที่ดีขึ้นโดยใช้รหัส Java เป็นไปได้ไหมที่คุณจะแบ่งปันรหัสแคช Java LRU ของคุณโดยใช้LinkedHashMapและCollections#synchronizedMap? ขณะนี้ฉันกำลังใช้งานLRUMap implements Mapและรหัสทำงานได้ดี แต่ฉันเริ่มArrayIndexOutofBoundExceptionทดสอบโหลดโดยใช้ผู้ใช้ 500 คนด้วยวิธีการด้านล่าง วิธีการย้ายวัตถุล่าสุดไปด้านหน้าของคิว

private void moveToFront(int index) {
        if (listHead != index) {
            int thisNext = nextElement[index];
            int thisPrev = prevElement[index];
            nextElement[thisPrev] = thisNext;
            if (thisNext >= 0) {
                prevElement[thisNext] = thisPrev;
            } else {
                listTail = thisPrev;
            }
            //old listHead and new listHead say new is 1 and old was 0 then prev[1]= 1 is the head now so no previ so -1
            // prev[0 old head] = new head right ; next[new head] = old head
            prevElement[index] = -1;
            nextElement[index] = listHead;
            prevElement[listHead] = index;
            listHead = index;
        }
    }

get(Object key)และput(Object key, Object value)วิธีการเรียกmoveToFrontวิธีการดังกล่าว


0

ต้องการที่จะเพิ่มความคิดเห็นในคำตอบที่ได้รับจากแฮงค์ แต่บางวิธีที่ฉันไม่สามารถ - โปรดปฏิบัติต่อมันเป็นความคิดเห็น

LinkedHashMap ดูแลรักษาคำสั่งการเข้าถึงเช่นกันโดยขึ้นอยู่กับพารามิเตอร์ที่ส่งผ่านในคอนสตรัคเตอร์มันจะเก็บรายการที่มีการเรียงสองเท่าเพื่อรักษาลำดับ (ดู LinkedHashMap.Entry)

@Pacerier นั้นถูกต้องที่ LinkedHashMap จะรักษาลำดับเดียวกันในขณะที่วนซ้ำถ้ามีการเพิ่มองค์ประกอบอีกครั้ง แต่เป็นเพียงในกรณีของโหมดคำสั่งแทรก

นี่คือสิ่งที่ฉันพบใน java เอกสารของวัตถุ LinkedHashMap.Entry

    /**
     * This method is invoked by the superclass whenever the value
     * of a pre-existing entry is read by Map.get or modified by Map.set.
     * If the enclosing Map is access-ordered, it moves the entry
     * to the end of the list; otherwise, it does nothing.
     */
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

วิธีนี้ดูแลการย้ายองค์ประกอบที่เข้าถึงล่าสุดไปยังจุดสิ้นสุดของรายการ ดังนั้นทั้งหมดใน LinkedHashMap ทั้งหมดจึงเป็นโครงสร้างข้อมูลที่ดีที่สุดสำหรับการใช้งาน LRUCache


0

ความคิดอื่นและแม้แต่การใช้งานง่ายโดยใช้ LinkedHashMap collection ของ Java

LinkedHashMap ให้วิธีการ removeEldestEntry และซึ่งสามารถแทนที่ในวิธีที่กล่าวถึงในตัวอย่าง โดยค่าเริ่มต้นการใช้งานโครงสร้างการรวบรวมนี้เป็นเท็จ หากความจริงและขนาดของโครงสร้างนี้เกินความจุเริ่มต้นกว่าองค์ประกอบที่เก่าหรือใหญ่กว่าจะถูกลบออก

เราสามารถมี pageno และเนื้อหาของหน้าในกรณีของฉัน pageno เป็นจำนวนเต็มและ pagecontent ฉันได้เก็บสตริงค่าหมายเลขหน้า

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author Deepak Singhvi
 *
 */
public class LRUCacheUsingLinkedHashMap {


     private static int CACHE_SIZE = 3;
     public static void main(String[] args) {
        System.out.println(" Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99");
        System.out.println("----------------------------------------------\n");


// accessOrder is true, so whenever any page gets changed or accessed,    // its order will change in the map, 
              LinkedHashMap<Integer,String> lruCache = new              
                 LinkedHashMap<Integer,String>(CACHE_SIZE, .75F, true) {

           private static final long serialVersionUID = 1L;

           protected boolean removeEldestEntry(Map.Entry<Integer,String>                           

                     eldest) {
                          return size() > CACHE_SIZE;
                     }

                };

  lruCache.put(2, "2");
  lruCache.put(1, "1");
  lruCache.put(0, "0");
  System.out.println(lruCache + "  , After first 3 pages in cache");
  lruCache.put(2, "2");
  System.out.println(lruCache + "  , Page 2 became the latest page in the cache");
  lruCache.put(8, "8");
  System.out.println(lruCache + "  , Adding page 8, which removes eldest element 2 ");
  lruCache.put(2, "2");
  System.out.println(lruCache+ "  , Page 2 became the latest page in the cache");
  lruCache.put(4, "4");
  System.out.println(lruCache+ "  , Adding page 4, which removes eldest element 1 ");
  lruCache.put(99, "99");
  System.out.println(lruCache + " , Adding page 99, which removes eldest element 8 ");

     }

}

ผลลัพธ์ของการเรียกใช้โค้ดข้างต้นมีดังนี้:

 Pages for consideration : 2, 1, 0, 2, 8, 2, 4,99
--------------------------------------------------
    {2=2, 1=1, 0=0}  , After first 3 pages in cache
    {2=2, 1=1, 0=0}  , Page 2 became the latest page in the cache
    {1=1, 0=0, 8=8}  , Adding page 8, which removes eldest element 2 
    {0=0, 8=8, 2=2}  , Page 2 became the latest page in the cache
    {8=8, 2=2, 4=4}  , Adding page 4, which removes eldest element 1 
    {2=2, 4=4, 99=99} , Adding page 99, which removes eldest element 8 

นั่นคือ FIFO เขาขอ LRU
RickHigh

มันล้มเหลวในการทดสอบนี้ ... cache.get (2); cache.get (3); cache.put (6, 6); cache.put (7, 7); ok | = cache.size () == 4 || ตาย ("ขนาด" + cache.size ()); ok | = cache.getSilent (2) == 2 || ตาย (); ok | = cache.getSilent (3) == 3 || ตาย (); ok | = cache.getSilent (4) == null || ตาย (); ok | = cache.getSilent (5) == null || ตาย ();
RickHigh

0

ทำตามแนวคิด @sanjanab (แต่หลังจากแก้ไขแล้ว) ฉันสร้าง LRUCache เวอร์ชันของฉันขึ้นมาพร้อมกับ Consumer ที่อนุญาตให้ทำบางสิ่งกับรายการที่ถูกลบหากจำเป็น

public class LRUCache<K, V> {

    private ConcurrentHashMap<K, V> map;
    private final Consumer<V> onRemove;
    private ConcurrentLinkedQueue<K> queue;
    private final int size;

    public LRUCache(int size, Consumer<V> onRemove) {
        this.size = size;
        this.onRemove = onRemove;
        this.map = new ConcurrentHashMap<>(size);
        this.queue = new ConcurrentLinkedQueue<>();
    }

    public V get(K key) {
        //Recently accessed, hence move it to the tail
        if (queue.remove(key)) {
            queue.add(key);
            return map.get(key);
        }
        return null;
    }

    public void put(K key, V value) {
        //ConcurrentHashMap doesn't allow null key or values
        if (key == null || value == null) throw new IllegalArgumentException("key and value cannot be null!");

        V existing = map.get(key);
        if (existing != null) {
            queue.remove(key);
            onRemove.accept(existing);
        }

        if (map.size() >= size) {
            K lruKey = queue.poll();
            if (lruKey != null) {
                V removed = map.remove(lruKey);
                onRemove.accept(removed);
            }
        }
        queue.add(key);
        map.put(key, value);
    }
}

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.