Làm thế nào bạn sẽ thực hiện một bộ đệm LRU trong Java?


169

Xin đừng nói EHCache hoặc OSCache, v.v. Giả sử cho các mục đích của câu hỏi này mà tôi muốn tự thực hiện bằng cách sử dụng SDK (vừa học vừa làm). Cho rằng bộ đệm sẽ được sử dụng trong môi trường đa luồng, bạn sẽ sử dụng cơ sở dữ liệu nào? Tôi đã triển khai một ứng dụng bằng LinkedHashMapBộ sưu tập # syncMap , nhưng tôi tò mò liệu có bất kỳ bộ sưu tập đồng thời mới nào sẽ là ứng cử viên tốt hơn không.

CẬP NHẬT: Tôi vừa đọc qua bài mới nhất của Yegge khi tôi tìm thấy cái nugget này:

Nếu bạn cần truy cập liên tục và muốn duy trì thứ tự chèn, bạn không thể làm tốt hơn LinkedHashMap, một cấu trúc dữ liệu thực sự tuyệt vời. Cách duy nhất có thể tuyệt vời hơn là nếu có một phiên bản đồng thời. Nhưng than ôi.

Tôi đã suy nghĩ gần như chính xác điều tương tự trước khi tôi thực hiện LinkedHashMap+ Collections#synchronizedMaptriển khai tôi đã đề cập ở trên. Rất vui được biết tôi đã không bỏ qua một cái gì đó.

Dựa trên các câu trả lời cho đến nay, có vẻ như sự đặt cược tốt nhất của tôi cho một LRU có tính đồng thời cao sẽ là mở rộng ConcảnHashMap bằng cách sử dụng một số logic tương tự LinkedHashMapsử dụng.


O(1)phiên bản bắt buộc: stackoverflow.com/questions/23772102/
Mạnh

Câu hỏi rất giống ở đây
Mifeet

Câu trả lời:


102

Tôi thích rất nhiều những gợi ý này, nhưng bây giờ tôi nghĩ tôi sẽ gắn bó với LinkedHashMap+ Collections.synchronizedMap. Nếu tôi xem lại điều này trong tương lai, có lẽ tôi sẽ làm việc mở rộng ConcurrentHashMaptheo cùng một cách LinkedHashMapmở rộng HashMap.

CẬP NHẬT:

Theo yêu cầu, đây là ý chính của việc thực hiện hiện tại của tôi.

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
Tuy nhiên tôi muốn sử dụng đóng gói ở đây thay vì thừa kế. Đây là điều tôi học được từ Java hiệu quả.
Kapil D

10
@KapilD Đã được một thời gian, nhưng tôi gần như tích cực với JavaDocs vì đã LinkedHashMapxác nhận rõ ràng phương thức này để tạo ra một triển khai LRU.
Hank Gay

7
@HankGay Java's LinkedHashMap (với tham số thứ ba = true) không phải là bộ đệm LRU. Điều này là do việc đặt lại một mục nhập không ảnh hưởng đến thứ tự của các mục nhập (bộ đệm LRU thực sẽ đặt mục nhập được chèn cuối cùng vào phía sau thứ tự lặp bất kể mục nhập đó có tồn tại trong bộ đệm hay không)
Pacerier

2
@Pacerier Tôi không thấy hành vi này chút nào. Với bản đồ được kích hoạt accessOrder, tất cả các hành động tạo một mục như được sử dụng gần đây nhất (mới nhất): chèn ban đầu, cập nhật giá trị và truy xuất giá trị. Tui bỏ lỡ điều gì vậy?
Esailija

3
@Pacerier "đặt lại một mục nhập không ảnh hưởng đến thứ tự của các mục", điều này không chính xác. Nếu bạn xem xét triển khai của LinkedHashMap, đối với phương thức "đặt", nó sẽ kế thừa triển khai từ HashMap. Và Javadoc của HashMap nói "Nếu bản đồ trước đây chứa ánh xạ cho khóa, giá trị cũ sẽ được thay thế". Và nếu bạn kiểm tra mã nguồn của nó, khi thay thế giá trị cũ, nó sẽ gọi phương thức recordAccess và trong phương thức recordAccess của LinkedHashMap, nó trông như thế này: if (lm.accessOrder) {lm.modCount ++; tẩy(); addB Before (lm.header);}
nybon


10

Đây là vòng hai.

Vòng đầu tiên là những gì tôi nghĩ ra sau đó tôi đọc lại các bình luận với tên miền ăn sâu hơn một chút trong đầu.

Vì vậy, đây là phiên bản đơn giản nhất với một bài kiểm tra đơn vị cho thấy nó hoạt động dựa trên một số phiên bản khác.

Đầu tiên là phiên bản không đồng thời:

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


}

Cờ thật sẽ theo dõi quyền truy cập của get và put. Xem JavaDocs. RemoveEdelstEntry không có cờ thực sự cho hàm tạo sẽ chỉ thực hiện bộ đệm FIFO (xem ghi chú bên dưới trên FIFO và removeEldestEntry).

Đây là bài kiểm tra chứng minh rằng nó hoạt động như một bộ đệm 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 ();

    }

Bây giờ cho phiên bản đồng thời ...

gói 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 ();
    }


}

Bạn có thể thấy lý do tại sao tôi bao gồm phiên bản không đồng thời đầu tiên. Các nỗ lực trên để tạo ra một số sọc để giảm sự tranh chấp khóa. Vì vậy, chúng tôi băm khóa và sau đó tìm kiếm băm đó để tìm bộ đệm thực tế. Điều này làm cho kích thước giới hạn nhiều hơn một gợi ý / phỏng đoán sơ bộ trong một lượng lỗi khá lớn tùy thuộc vào mức độ lan truyền của thuật toán băm khóa của bạn.

Dưới đây là thử nghiệm để chỉ ra rằng phiên bản đồng thời có thể hoạt động. :) (Thử nghiệm dưới lửa sẽ là cách thực sự).

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 ();



    }

}

Đây là bài đăng cuối cùng .. Bài đăng đầu tiên tôi đã xóa vì nó là LFU không phải là bộ đệm LRU.

Tôi nghĩ rằng tôi sẽ cho điều này một lần nữa đi. Tôi đã cố gắng đưa ra phiên bản đơn giản nhất của bộ đệm LRU bằng cách sử dụng JDK tiêu chuẩn với quá nhiều triển khai.

Đây là những gì tôi đã đưa ra. Nỗ lực đầu tiên của tôi là một chút thảm họa khi tôi thực hiện LFU thay vì và LRU, và sau đó tôi đã thêm hỗ trợ của FIFO và LRU cho nó ... và sau đó tôi nhận ra nó đang trở thành một con quái vật. Sau đó, tôi bắt đầu nói chuyện với John, bạn thân của tôi, người gần như không quan tâm, và sau đó tôi đã mô tả sâu về cách tôi triển khai LFU, LRU và FIFO và cách bạn có thể chuyển đổi nó bằng một đối số ENUM đơn giản, và sau đó tôi nhận ra rằng tất cả những gì tôi thực sự muốn là một LRU đơn giản. Vì vậy, bỏ qua bài đăng trước đó từ tôi và cho tôi biết nếu bạn muốn xem bộ đệm ẩn LRU / LFU / FIFO có thể chuyển đổi thông qua một enum ... không? Được rồi .. anh đi đây.

LRU đơn giản nhất có thể chỉ sử dụng JDK. Tôi đã triển khai cả phiên bản đồng thời và phiên bản không đồng thời.

Tôi đã tạo một giao diện chung (đó là chủ nghĩa tối giản nên có thể thiếu một vài tính năng mà bạn muốn nhưng nó hoạt động cho các trường hợp sử dụng của tôi, nhưng hãy để nếu bạn muốn xem tính năng XYZ hãy cho tôi biết ... Tôi sống để viết mã.) .

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

Bạn có thể tự hỏi những gì getSilent là . Tôi sử dụng điều này để thử nghiệm. getSilent không thay đổi điểm số LRU của một mặt hàng.

Đầu tiên là không đồng thời ....

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

Hàng đợi.removeFirstOccurrence là một hoạt động có khả năng tốn kém nếu bạn có bộ đệm lớn. Người ta có thể lấy LinkedList làm ví dụ và thêm bản đồ băm tra cứu ngược từ phần tử sang nút để thực hiện các thao tác loại bỏ RẤT NHIỀU và phù hợp hơn. Tôi cũng bắt đầu, nhưng rồi nhận ra mình không cần nó. Nhưng ... có lẽ ...

Khi đặt được gọi, khóa được thêm vào hàng đợi. Khi nhận được gọi, khóa sẽ được gỡ bỏ và thêm lại vào đầu hàng đợi.

Nếu bộ nhớ cache của bạn nhỏ và việc xây dựng một mục đắt tiền thì đây sẽ là một bộ đệm tốt. Nếu bộ nhớ cache của bạn thực sự lớn, thì tìm kiếm tuyến tính có thể là cổ chai đặc biệt là nếu bạn không có vùng bộ đệm nóng. Các điểm nóng càng dữ dội, tìm kiếm tuyến tính càng nhanh vì các mục nóng luôn ở đầu tìm kiếm tuyến tính. Dù sao ... những gì cần thiết để làm điều này nhanh hơn là viết một LinkedList khác có thao tác gỡ bỏ có phần tử đảo ngược để tra cứu nút, sau đó xóa sẽ nhanh như xóa một khóa khỏi bản đồ băm.

Nếu bạn có bộ đệm dưới 1.000 mục, điều này sẽ hoạt động tốt.

Đây là một thử nghiệm đơn giản để hiển thị các hoạt động của nó trong hành động.

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 ();

    }
}

Bộ nhớ cache LRU cuối cùng được phân luồng đơn và xin vui lòng không bọc nó trong bất kỳ thứ gì được đồng bộ hóa ....

Đây là một đâm ở một phiên bản đồng thời.

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

Sự khác biệt chính là việc sử dụng ConcảnHashMap thay vì HashMap và sử dụng Khóa (Tôi có thể đã nhận được đồng bộ hóa, nhưng ...).

Tôi chưa thử nó dưới lửa, nhưng có vẻ như bộ đệm LRU đơn giản có thể hoạt động trong 80% trường hợp sử dụng khi bạn cần bản đồ LRU đơn giản.

Tôi hoan nghênh phản hồi, ngoại trừ lý do tại sao bạn không sử dụng thư viện a, b hoặc c. Lý do tôi không luôn sử dụng thư viện là vì tôi không muốn mọi tệp tin chiến tranh là 80 MB và tôi viết thư viện để tôi có thể tạo các libs có thể cắm với một giải pháp đủ tốt và ai đó có thể cắm -Trong nhà cung cấp bộ đệm khác nếu họ thích. :) Tôi không bao giờ biết khi nào ai đó có thể cần Guava hoặc ehcache hoặc thứ gì khác mà tôi không muốn bao gồm chúng, nhưng nếu tôi tạo bộ nhớ đệm có thể cắm, tôi cũng sẽ không loại trừ chúng.

Giảm sự phụ thuộc có phần thưởng riêng của nó. Tôi thích nhận được một số phản hồi về cách làm cho điều này thậm chí đơn giản hơn hoặc nhanh hơn hoặc cả hai.

Ngoài ra nếu có ai biết sẵn sàng để đi ....

Ok .. Tôi biết bạn đang nghĩ gì ... Tại sao anh ấy không sử dụng removeEldest entry từ LinkedHashMap, và tôi cũng nên nhưng .... nhưng .. nhưng đó sẽ là một FIFO không phải là một LRU và chúng tôi đã cố gắng thực hiện một LRU.

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

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

Thử nghiệm này không thành công cho mã trên ...

        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 ();

Vì vậy, đây là một bộ đệm FIFO nhanh và bẩn bằng cách sử dụng 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 là nhanh chóng. Không tìm kiếm xung quanh. Bạn có thể đứng trước một FIFO trước một LRU và điều đó sẽ xử lý hầu hết các mục nóng khá độc đáo. Một LRU tốt hơn sẽ cần yếu tố đảo ngược đó đến tính năng Node.

Dù sao ... bây giờ tôi đã viết một số mã, hãy để tôi xem qua các câu trả lời khác và xem những gì tôi đã bỏ lỡ ... lần đầu tiên tôi quét chúng.


9

LinkedHashMaplà O (1), nhưng yêu cầu đồng bộ hóa. Không cần phải phát minh lại bánh xe ở đó.

2 tùy chọn để tăng đồng thời:

1. Tạo nhiều LinkedHashMapvà băm vào chúng: ví dụ : LinkedHashMap[4], index 0, 1, 2, 3. Trên phím làm key%4 (hoặc binary ORbật [key, 3]) để chọn bản đồ cần thực hiện đặt / nhận / xóa.

2. Bạn có thể thực hiện một "gần như" LRU bằng cách mở rộng ConcurrentHashMapvà có một bản đồ băm được liên kết giống như cấu trúc ở mỗi khu vực bên trong nó. Khóa sẽ xảy ra chi tiết hơn so với khóa LinkedHashMapđược đồng bộ hóa. Trên một puthoặc putIfAbsentchỉ một khóa trên đầu và đuôi của danh sách là cần thiết (mỗi vùng). Khi gỡ bỏ hoặc lấy toàn bộ khu vực cần phải được khóa. Tôi tò mò nếu các danh sách liên kết nguyên tử của một số loại có thể giúp đỡ ở đây - có lẽ là như vậy cho người đứng đầu danh sách. Có lẽ để biết thêm.

Cấu trúc sẽ không giữ tổng thứ tự, mà chỉ thứ tự cho mỗi vùng. Miễn là số lượng mục lớn hơn nhiều so với số vùng, điều này đủ tốt cho hầu hết các bộ nhớ cache. Mỗi khu vực sẽ phải có số lượng mục nhập riêng, điều này sẽ được sử dụng thay vì tổng số toàn cầu cho trình kích hoạt trục xuất. Số vùng mặc định trong a ConcurrentHashMaplà 16, rất nhiều cho hầu hết các máy chủ hiện nay.

  1. sẽ dễ dàng hơn để viết và nhanh hơn dưới sự đồng thời vừa phải.

  2. sẽ khó viết hơn nhưng quy mô tốt hơn nhiều ở mức độ đồng thời rất cao. Nó sẽ chậm hơn đối với truy cập bình thường (cũng như ConcurrentHashMapchậm hơn so với HashMapnơi không có sự tương tranh)


8

Có hai triển khai nguồn mở.

Apache Solr có đồng thờiLRUCache: https://lucene.apache.org/solr/3_6_1/org/apache/solr/util/ConcienLRUCache.html

Có một dự án nguồn mở cho một concurrencyLinkedHashMap: http://code.google.com.vn/p/concienlinkedhashmap/


2
Giải pháp của Solr không thực sự là LRU, nhưng ConcurrentLinkedHashMapthật thú vị. Nó tuyên bố đã được đưa vào MapMakertừ Guava, nhưng tôi đã không phát hiện ra nó trong các tài liệu. Bất cứ ý tưởng những gì đang đi với nỗ lực đó?
Hank Gay

3
Một phiên bản đơn giản hóa đã được tích hợp, nhưng các bài kiểm tra chưa được hoàn thành nên chưa được công khai. Tôi đã có rất nhiều vấn đề khi thực hiện tích hợp sâu hơn, nhưng tôi hy vọng sẽ hoàn thành nó vì có một số thuộc tính thuật toán tốt. Khả năng lắng nghe một sự trục xuất (công suất, hết hạn, GC) đã được thêm vào và dựa trên phương pháp của CLHM (hàng đợi người nghe). Tôi cũng muốn đóng góp ý tưởng về "giá trị trọng số", vì điều đó rất hữu ích khi lưu trữ các bộ sưu tập. Thật không may do các cam kết khác, tôi đã quá lúng túng để dành thời gian Guava xứng đáng (và tôi đã hứa với Kevin / Charles).
Ben Manes

3
Cập nhật: Việc tích hợp đã hoàn tất và công khai trong Guava r08. Điều này thông qua cài đặt #maximumSize ().
Ben Manes

7

Tôi sẽ xem xét sử dụng java.util.concản.P WarriorityBlockingQueue , với mức độ ưu tiên được xác định bởi bộ đếm "numberOfUses" trong mỗi phần tử. Tôi sẽ rất, rất cẩn thận để có được tất cả đồng bộ hóa chính xác của mình, vì bộ đếm "numberOfUses" ngụ ý rằng phần tử không thể thay đổi được.

Đối tượng phần tử sẽ là một trình bao bọc cho các đối tượng trong bộ đệm:

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

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

    ... etc.
}

không có nghĩa là bạn phải bất biến?
shsteimer

2
lưu ý rằng nếu bạn cố gắng thực hiện phiên bản Priorblockingqueue được đề cập bởi steve mcleod, bạn nên làm cho phần tử không thay đổi, bởi vì sửa đổi phần tử trong hàng đợi sẽ không ảnh hưởng, bạn sẽ cần xóa phần tử và thêm lại phần tử đó để ưu tiên lại nó.
James

James dưới đây chỉ ra một lỗi tôi đã làm. Mà tôi đưa ra làm bằng chứng cho việc làm thế nào khó khăn để viết những bộ đệm mạnh mẽ, đáng tin cậy.
Steve McLeod

6

Hi vọng điêu nay co ich .

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
Ví dụ hay! Bạn có thể nhận xét tại sao cần phải thiết lập dung lượng maxSize * 4/3 không?
Akvel

1
@Akvel được gọi là công suất ban đầu, có thể là bất kỳ giá trị [số nguyên] nào trong khi 0,75f là hệ số tải mặc định, hy vọng liên kết này sẽ giúp: ashishsharma.me/2011/09/custom-lru-cache-java.html
mờ

5

Bộ nhớ cache LRU có thể được triển khai bằng cách sử dụng một concienLinkedQueue và một concienHashMap cũng có thể được sử dụng trong kịch bản đa luồng. Đầu của hàng đợi là phần tử đã ở trên hàng đợi lâu nhất. Đuôi của hàng đợi là phần tử đã ở trên hàng đợi trong thời gian ngắn nhất. Khi một phần tử tồn tại trong Bản đồ, chúng ta có thể xóa nó khỏi LinkedQueue và chèn nó vào phần đuôi.

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

}

Đây không phải là chủ đề an toàn. Ví dụ, bạn có thể dễ dàng vượt quá kích thước LRU tối đa bằng cách gọi đồng thời put.
dpeacock

Xin vui lòng sửa lại. Trước hết, nó không biên dịch trên dòng map.containsKey (key). Thứ hai trong get () bạn nên kiểm tra xem khóa có thực sự bị xóa hay không nếu không ánh xạ và hàng đợi không đồng bộ hóa và "queue.size ()> = size" luôn luôn đúng. Tôi sẽ đăng phiên bản của mình có bản sửa lỗi này vì tôi thích ý tưởng của bạn về việc sử dụng hai bộ sưu tập này.
Aleksander Lech

3

Đây là triển khai của tôi cho LRU. Tôi đã sử dụng PriorityQueue, về cơ bản hoạt động như là FIFO và không phải là chủ đề an toàn. Bộ so sánh được sử dụng dựa trên việc tạo thời gian trang và dựa trên việc thực hiện thứ tự các trang trong thời gian ít sử dụng gần đây nhất.

Các trang để xem xét: 2, 1, 0, 2, 8, 2, 4

Trang được thêm vào bộ đệm là: 2
Trang được thêm vào bộ đệm là: 1
Trang được thêm vào bộ đệm là: 0
Trang: 2 đã xuất hiện trong bộ đệm. Thời gian truy cập được cập nhật lần cuối
Lỗi trang, TRANG: 1, Thay thế bằng TRANG: 8
Trang được thêm vào bộ đệm là: 8
Trang: 2 đã xuất hiện trong bộ đệm. Thời gian truy cập được cập nhật lần cuối
Lỗi trang, TRANG: 0, Thay thế bằng TRANG: 4
Trang được thêm vào bộ đệm là: 4

ĐẦU RA

Các trang LRUCache
-------------
PageName: 8, PageCreationTime: 1365957019974
PageName: 2, PageCreationTime: 1365957020074
PageName: 4, PageCreationTime: 1365957020174

nhập mã vào đây

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

Dưới đây là thử nghiệm thực hiện bộ đệm LRU đồng thời được thực hiện tốt nhất của tôi mà không có bất kỳ khối đồng bộ hóa nào:

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 .... bạn chưa xử lý một tình huống .. nếu cùng một đối tượng được sử dụng nhiều lần thì sao? trong trường hợp này, chúng ta không nên thêm nhiều mục cho cùng một đối tượng ... thay vào đó, khóa của nó phải là

5
Cảnh báo: Đây không phải là bộ đệm LRU. Trong bộ đệm LRU, bạn vứt bỏ các mục ít truy cập gần đây nhất. Điều này ném đi các mục ít được viết gần đây. Đây cũng là một quét tuyến tính để thực hiện thao tác queue.remove (key).
Dave L.

Ngoài ra, concienLinkedQueue # size () không phải là một hoạt động thời gian không đổi.
NateS

3
Phương thức đặt của bạn trông không an toàn - nó có một vài câu lệnh kiểm tra hành động sẽ phá vỡ với nhiều luồng.
assylias

2

Đây là bộ đệm LRU mà tôi sử dụng, nó đóng gói LinkedHashMap và xử lý đồng thời với một khóa đồng bộ hóa đơn giản bảo vệ các điểm ngon ngọt. Nó "chạm" vào các phần tử khi chúng được sử dụng để chúng trở thành phần tử "mới nhất" một lần nữa, do đó nó thực sự là LRU. Tôi cũng có yêu cầu các yếu tố của tôi có tuổi thọ tối thiểu, mà bạn cũng có thể nghĩ là "thời gian nhàn rỗi tối đa" được phép, sau đó bạn sẽ bị trục xuất.

Tuy nhiên, tôi đồng ý với kết luận của Hank và câu trả lời được chấp nhận - nếu tôi bắt đầu lại từ hôm nay, tôi sẽ kiểm tra Guava 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

Vâng, đối với bộ đệm, bạn thường sẽ tìm kiếm một số dữ liệu thông qua một đối tượng proxy, (một URL, Chuỗi ....) để giao diện khôn ngoan mà bạn sẽ muốn có một bản đồ. nhưng để loại bỏ mọi thứ bạn muốn có một cấu trúc như hàng đợi. Trong nội bộ, tôi sẽ duy trì hai cấu trúc dữ liệu, Ưu tiên-Hàng đợi và HashMap. Đây là một triển khai có thể thực hiện mọi thứ trong thời gian O (1).

Đây là một lớp học tôi đã đánh lên khá nhanh:

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

Đây là cách nó hoạt động. Các khóa được lưu trữ trong một danh sách được liên kết với các khóa cũ nhất ở phía trước danh sách (các phím mới đi về phía sau), vì vậy khi bạn cần 'nhả' thứ gì đó, bạn chỉ cần bật nó ra trước hàng đợi và sau đó sử dụng phím để xóa giá trị khỏi bản đồ. Khi một mục được tham chiếu, bạn lấy ValueHolder từ bản đồ và sau đó sử dụng biến xếp hàng để loại bỏ khóa khỏi vị trí hiện tại của nó trong hàng đợi và sau đó đặt nó ở phía sau hàng đợi (hiện được sử dụng gần đây nhất). Thêm mọi thứ là khá nhiều như nhau.

Tôi chắc chắn có rất nhiều lỗi ở đây và tôi đã không thực hiện bất kỳ đồng bộ hóa nào. nhưng lớp này sẽ cung cấp O (1) thêm vào bộ đệm, loại bỏ O (1) các mục cũ và truy xuất O (1) các mục bộ đệm. Ngay cả một đồng bộ hóa tầm thường (chỉ đồng bộ hóa mọi phương thức công khai) vẫn sẽ có ít sự tranh chấp khóa do thời gian chạy. Nếu bất cứ ai có bất kỳ thủ thuật đồng bộ hóa thông minh, tôi sẽ rất quan tâm. Ngoài ra, tôi chắc chắn có một số tối ưu hóa bổ sung mà bạn có thể thực hiện bằng cách sử dụng biến maxsize đối với bản đồ.


Cảm ơn về mức độ chi tiết, nhưng điều này mang lại chiến thắng cho việc thực hiện LinkedHashMap+ ở Collections.synchronizedMap()đâu?
Hank Gay

Hiệu suất, tôi không biết chắc chắn, nhưng tôi không nghĩ LinkedHashMap có chèn O (1) (có lẽ là O (log (n))), thực sự bạn có thể thêm một vài phương thức để hoàn thành giao diện bản đồ khi triển khai và sau đó sử dụng Collections.syn syncizedMap để thêm đồng thời.
luke

Trong lớp LinkedList ở trên trong phương thức add, có một mã trong khối khác tức là if (tail.prev == null) {tail.prev = head; đầu.next = đuôi; } Khi nào mã này sẽ được thực thi? Tôi đã chạy một vài lần chạy khô và tôi nghĩ rằng điều này sẽ không bao giờ được thực hiện và nên được gỡ bỏ.
Dipesh

1

Có một cái nhìn về ConcurrencySkipListMap . Nó sẽ cung cấp cho bạn thời gian đăng nhập (n) để kiểm tra và xóa phần tử nếu nó đã được chứa trong bộ đệm và thời gian liên tục để thêm lại nó.

Bạn chỉ cần một số bộ đếm vv và phần tử trình bao bọc để buộc đặt hàng theo thứ tự LRU và đảm bảo các nội dung gần đây bị loại bỏ khi bộ đệm đầy.


Sẽ ConcurrentSkipListMapcung cấp một số lợi ích dễ thực hiện hơn ConcurrentHashMap, hoặc nó chỉ đơn giản là một trường hợp tránh các trường hợp bệnh lý?
Hank Gay

Nó sẽ làm cho mọi thứ đơn giản hơn, vì các phần tử đơn đặt hàng của ConcảnSkipListMap, cho phép bạn quản lý thứ tự được sử dụng trong. Đồng thời đã sử dụng bộ đếm 'hoặc bất cứ điều gì
madlep

Vì vậy, với việc ConcurrentSkipListMaptriển khai, tôi sẽ tạo ra một triển khai mới của Mapgiao diện ủy nhiệm ConcurrentSkipListMapvà thực hiện một số loại gói sao cho các loại khóa tùy ý được gói trong một loại có thể dễ dàng sắp xếp dựa trên lần truy cập cuối cùng?
Hank Gay

1

Đây là triển khai ngắn của tôi, xin vui lòng chỉ trích hoặc cải thiện nó!

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
Đây không phải là bộ đệm LRU, chỉ là bộ đệm FIFO.
lslab

1

Đây là cách thực hiện của riêng tôi cho vấn đề này

Simplelrucache cung cấp bộ nhớ đệm LRU an toàn, rất đơn giản, không phân phối với sự hỗ trợ của TTL. Nó cung cấp hai triển khai:

  • Đồng thời dựa trên concurrencyLinkedHashMap
  • Được đồng bộ hóa dựa trên LinkedHashMap

Bạn có thể tìm thấy nó ở đây: http://code.google.com.vn/p/simplelrucache/


1

Cách tốt nhất để đạt được là sử dụng LinkedHashMap duy trì thứ tự chèn các phần tử. Sau đây là một mã mẫu:

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

Tôi đang tìm kiếm một bộ đệm LRU tốt hơn bằng cách sử dụng mã Java. Bạn có thể chia sẻ mã bộ đệm Java LRU của mình bằng cách sử dụng LinkedHashMapCollections#synchronizedMapkhông? Hiện tại tôi đang sử dụng LRUMap implements Mapvà mã hoạt động tốt, nhưng tôi đang ArrayIndexOutofBoundExceptionkiểm tra tải bằng 500 người dùng theo phương pháp dưới đây. Phương thức di chuyển đối tượng gần đây ra phía trước hàng đợi.

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)phương thức gọi moveToFrontphương thức trên .


0

Muốn thêm nhận xét vào câu trả lời do Hank đưa ra nhưng một số cách tôi không thể - vui lòng coi đó là nhận xét

LinkedHashMap duy trì thứ tự truy cập cũng dựa trên tham số được truyền trong hàm tạo của nó Nó giữ danh sách xếp hàng gấp đôi để duy trì trật tự (Xem LinkedHashMap.Entry)

@Pacerier đúng là LinkedHashMap giữ nguyên thứ tự trong khi lặp nếu phần tử được thêm lại nhưng đó chỉ là trong trường hợp chế độ đặt hàng chèn.

đây là những gì tôi tìm thấy trong các tài liệu java của đối tượng 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);
        }
    }

phương pháp này đảm nhiệm việc di chuyển phần tử được truy cập gần đây vào cuối danh sách. Vì vậy, tất cả trong LinkedHashMap là cấu trúc dữ liệu tốt nhất để triển khai LRUCache.


0

Một suy nghĩ khác và thậm chí là một triển khai đơn giản bằng cách sử dụng bộ sưu tập Java của LinkedHashMap.

LinkedHashMap cung cấp phương thức removeEldestEntry và có thể được ghi đè theo cách được đề cập trong ví dụ. Theo mặc định thực hiện cấu trúc bộ sưu tập này là sai. Nếu sự thật và kích thước của cấu trúc này vượt quá khả năng ban đầu thì các phần tử cũ nhất hoặc cũ hơn sẽ bị loại bỏ.

Chúng tôi có thể có một nội dung pageno và trang trong trường hợp của tôi pageno là số nguyên và pagecontent tôi đã giữ chuỗi giá trị số trang.

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 ");

     }

}

Kết quả thực hiện mã trên như sau:

 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 

Đó là một năm. Anh ta yêu cầu một LRU.
RickHigh

Nó thất bại trong bài kiểm tra này ... cache.get (2); cache.get (3); cache.put (6, 6); cache.put (7, 7); ok | = cache.size () == 4 || chết ("kích thước" + cache.size ()); ok | = cache.getSilent (2) == 2 || chết (); ok | = cache.getSilent (3) == 3 || chết (); ok | = cache.getSilent (4) == null || chết (); ok | = cache.getSilent (5) == null || chết ();
RickHigh

0

Theo khái niệm @sanjanab (nhưng sau khi sửa lỗi), tôi đã tạo phiên bản LRUCache cung cấp cho Người tiêu dùng cho phép thực hiện điều gì đó với các mục đã xóa nếu cần.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.