如何在Java中实现LRU缓存?

175
请不要提EHCache或OSCache等缓存工具。为了实践学习,假设我想使用SDK自己实现一个缓存。鉴于该缓存将在多线程环境中使用,哪些数据结构会更合适?我已经使用LinkedHashMapCollections#synchronizedMap实现了其中一个,但我很好奇是否有任何新的并发集合会更好。
更新:我刚刚在阅读Yegge's latest时发现了这个重要信息:
如果你需要保持插入顺序并且需要恒定时间访问,则LinkedHashMap是最好的选择,它是一种非常好的数据结构。唯一可能使其更好的方式是如果有一个并发版本。但不幸的是。
在我选择上面提到的LinkedHashMap+ Collections#synchronizedMap实现之前,我几乎也在考虑同样的事情。很高兴知道我没有漏掉什么。
根据迄今为止的答案,似乎对于高度并发的LRU,最好的选择是使用ConcurrentHashMap扩展一些与LinkedHashMap相同的逻辑。

O(1) 需求版本:https://dev59.com/dGAg5IYBdhLWcg3wSpTD - Ciro Santilli OurBigBook.com
这里也有非常类似的问题:[https://dev59.com/TnVC5IYBdhLWcg3wpS7g]。 - Mifeet
一个LRU实现示例:这里 - piepi
21个回答

105

我喜欢这些建议,但是目前我想使用 LinkedHashMap + Collections.synchronizedMap。如果未来需要重新考虑,我可能会像 LinkedHashMap 扩展 HashMap 一样扩展 ConcurrentHashMap

更新:

按照要求,这是我的当前实现要点。

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
12
已经有一段时间了,但我几乎可以确定LinkedHashMap的JavaDocs明确支持使用这种方法来创建LRU实现。 - Hank Gay
7
Java的LinkedHashMap(第三个参数为true)不是一个LRU缓存。这是因为重新放置一个条目并不会影响条目的顺序(真正的LRU缓存会将最后插入的条目置于迭代顺序的末尾,无论该条目最初是否存在于缓存中)。 - Pacerier
4
“重新放置一个条目不会影响条目的顺序”,这是不正确的。如果您查看LinkedHashMap的实现,对于“put”方法,它继承了HashMap的实现。而HashMap的Javadoc中写道:“如果映射先前包含键的映射,则旧值将被替换。” 如果您查看其源代码,在替换旧值时,它将调用recordAccess方法,并且在LinkedHashMap的recordAccess方法中,它看起来像这样:如果(lm.accessOrder){lm.modCount ++; remove(); addBefore(lm.header);} - nybon
3
@nybon 我知道缓存的最大大小,那么为什么我会想要任何负载因子不是 1.0f呢?这样做的目的是只有一个分配(初始分配),并在必须增长以容纳新元素时避免重新哈希Map的现有内容。 - Hank Gay
显示剩余10条评论

19
如果我今天从零开始再做一次,我会使用Guava的CacheBuilder

11

这是第二轮。

第一轮是我想出来的,然后我再次阅读了与该域有关的注释,以便更深入地理解它。

所以这里是最简单的版本,带有一个单元测试,显示它基于其他版本可以工作。

首先是非并发版本:

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


}

真正的标志将跟踪gets和puts的访问。请参阅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缓存。

我想再尝试一下。我试图使用标准JDK来设计最简单的LRU缓存版本,而不需要太多的实现。

这是我的想法。我的第一次尝试有些失败,因为我实现了一个LFU而不是LRU,然后我添加了FIFO和LRU支持... 然后我意识到它正在变成一个庞然大物。然后我开始和我的好友约翰交谈,他对此事并不是很感兴趣,然后我详细描述了如何实现LFU、LRU和FIFO以及如何用简单的ENUM arg进行切换,然后我意识到我真正想要的只是一个简单的LRU。所以请忽略我之前的帖子,如果你想看到一个可以通过枚举变量切换LRU/LFU/FIFO缓存的实现,请让我知道... 不想看?好吧... 下面开始。

这是只使用标准JDK实现的最简单的LRU。我同时实现了并发版本和非并发版本。

我创建了一个共同的接口(它非常简洁,可能缺少您想要的一些功能,但它可以满足我的使用案例,如果您想看到功能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为例,并添加一个反向查找哈希映射从元素到节点,使删除操作更快,更一致。我也曾开始过,但后来意识到我不需要它。但是...也许...

当调用put时,键被添加到队列中。当调用get时,键被移除并重新添加到队列顶部。

如果您的缓存很小且构建项很昂贵,则这应该是一个很好的缓存。 如果您的缓存非常大,则线性搜索可能成为瓶颈,特别是如果您没有热点缓存区域。 热点越强烈,线性搜索就越快,因为热点始终位于线性搜索的顶部。 无论如何...要使其更快,需要编写另一个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(我本可以只用synchronized,但是……)。
我还没有在实际应用中测试过它,但是它似乎是一个简单的LRU缓存,可能适用于80%需要简单LRU映射的使用情况。
欢迎反馈,除了为什么不使用库a、b或c之外。 我不总是使用库的原因是我不想让每个war文件都有80MB,而且我写库时候通常会使库可插拔,当然也可以换成其他缓存提供者。 :) 我永远不知道什么时候有人可能需要Guava或ehcache或其他我不想包含的东西,但如果我让缓存可插拔,我也不会将它们排除在外。
减少依赖关系有其自己的奖励。 我很乐意听取如何使这更简单或更快或两者兼备的反馈。
此外,如果有人知道现成的……。
好的……我知道你在想什么……为什么他不使用LinkedHashMap的removeEldestEntry呢,嗯,我应该使用,但是……但是……那将是一个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 ();

这里有一个使用removeEldestEntry实现的快速而简单的FIFO缓存。

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 需要具备反向节点功能。

无论如何,既然我已经写了一些代码,让我再看看其他答案,看看我错过了什么......第一次我只是粗略地浏览了它们。


10
LinkedHashMap 是 O(1) 的,但需要同步。不需要重新发明轮子。
增加并发性的两个选项:
1.创建多个 LinkedHashMap,并将其哈希到它们中: 示例:LinkedHashMap[4],索引 0、1、2、3。在键上执行 key%4(或 [key,3] 上的二进制 OR)以选择要进行 put/get/remove 操作的地图。
2.通过扩展 ConcurrentHashMap 并在其中的每个区域中使用类似于链接哈希图的结构来进行“几乎”LRU。锁定将比同步的 LinkedHashMap 更细粒度。在 putputIfAbsent 上,仅需要锁定列表头和尾(每个区域)。在 remove 或 get 上,整个区域都需要被锁定。我很好奇一些原子链表是否可以在这里帮助——对于列表头来说可能是这样的。也许还有更多。
该结构不会保留总顺序,而只保留每个区域的顺序。只要条目数远大于区域数,这对大多数缓存来说就足够了。每个区域都必须有自己的条目计数,这将用于驱逐触发而不是全局计数。ConcurrentHashMap 的默认区域数为 16,对于今天的大多数服务器来说已经足够了。
1.在中度并发下,编写起来更容易,速度更快。 2.编写起来更困难,但在非常高的并发下可以更好地扩展。它在正常访问时会更慢(就像没有并发的情况下 ConcurrentHashMap 比 HashMap 更慢)

8

2
Solr的解决方案实际上并不是LRU,但ConcurrentLinkedHashMap很有趣。它声称已经被整合到Guava的MapMaker中,但我在文档中没有找到它。你知道这个努力的进展吗? - Hank Gay
3
已经整合了一个简化版本,但是测试还没有完成,所以它还没有公开。我在进行更深层次的整合时遇到了很多问题,但是我希望能完成它,因为它具有一些不错的算法特性。添加了监听驱逐(容量、过期、GC)的功能,其基于CLHM的方法(监听器队列)。我也想贡献“加权值”这个想法,因为它在缓存集合时非常有用。不幸的是,由于其他承诺使我太忙了,无法为Guava奉献出应有的时间(这是我向Kevin/Charles承诺的)。 - Ben Manes
3
更新:Guava r08 中已经完成了集成并公开。这是通过 #maximumSize() 设置实现的。 - Ben Manes

7
我建议使用java.util.concurrent.PriorityBlockingQueue,通过每个元素中的“使用次数”计数器来确定优先级。但需要非常小心地确保所有同步都正确,因为“使用次数”计数器意味着该元素不能是不可变的。
元素对象将是缓存中对象的包装器:
class CacheElement {
    private final Object obj;
    private int numberOfUsers = 0;

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

    ... etc.
}

你是不是想说必须是不可变的? - shsteimer
2
请注意,如果您尝试使用Steve McLeod提到的PriorityBlockingQueue版本,则应使元素不可变,因为在队列中修改元素将没有影响,您需要删除该元素并重新添加以重新设置优先级。 - james
James指出了我的错误。这也证明了编写可靠、稳定的缓存有多么困难。 - Steve McLeod

7
希望这可以帮到你。
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
@Akvel,这被称为初始容量,可以是任何[整数]值,而0.75f是默认的负载因子,希望这个链接有所帮助:http://www.ashishsharma.me/2011/09/custom-lru-cache-java.html。 - murasing

5

可以使用ConcurrentLinkedQueue和ConcurrentHashMap实现LRU缓存,这种实现方式也适用于多线程场景。队列的头部是队列中存在时间最长的元素。队列的尾部是队列中存在时间最短的元素。当一个元素存在于Map中时,我们可以从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);
  }

}

这个不是线程安全的。例如,通过同时调用“put”可以轻松地超出最大LRU大小。 - dpeacock
首先,在 line map.containsKey(key) 上它无法编译。其次,在 get() 中,您应该检查 key 是否已被删除,否则 map 和 queue 将不同步,并且 "queue.size() >= size" 将始终为 true。我会发布一个修复了这个问题的版本,因为我喜欢您使用这两个集合的想法。 - Aleksander Lech

3

这是我的LRU实现。我使用了PriorityQueue,它基本上作为FIFO工作,但不是线程安全的。使用基于页面创建时间的Comparator,并根据最近未使用时间对页面进行排序。

考虑的页面:2、1、0、2、8、2、4

添加到缓存中的页面是:2
添加到缓存中的页面是:1
添加到缓存中的页面是:0
页面2已经存在于缓存中。更新最后访问时间
页面错误,页面1已被页面8替换
添加到缓存中的页面是:8
页面2已经存在于缓存中。更新最后访问时间
页面错误,页面0已被页面4替换
添加到缓存中的页面是:4

输出

LRUCache 页面
-------------
页面名称:8,页面创建时间:1365957019974
页面名称:2,页面创建时间:1365957020074
页面名称:4,页面创建时间: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....你还没有处理一种情况..如果同一个对象被多次使用怎么办?在这种情况下,我们不应该为同一个对象添加多个条目...相反,它的键应该是什么。 - user997347
5
警告:这不是一个LRU缓存。在LRU缓存中,你会扔掉最近最少使用的项目。而这个缓存会扔掉最近最少写入的项目。执行queue.remove(key)操作也需要进行线性扫描。 - Dave L.
另外,ConcurrentLinkedQueue#size()不是一个常数时间操作。 - NateS
3
您的put方法看起来不太安全 - 它有几个检查然后执行语句,在多个线程下会出现问题。 - assylias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接