2013-04-11 161 views
3

我想创建一个有效的LRU缓存实现。我发现最方便的方法是使用LinkedHashMap,但不幸的是,如果许多线程正在使用缓存,它会很慢。我的实现是在这里:实现LRU缓存的最好方法

/** 
* Class provides API for FixedSizeCache. 
* Its inheritors represent classes   
* with concrete strategies  
* for choosing elements to delete 
* in case of cache overflow. All inheritors 
* must implement {@link #getSize(K, V)}. 
*/ 
public abstract class FixedSizeCache <K, V> implements ICache <K, V> { 
    /** 
    * Current cache size. 
    */ 
    private int currentSize; 


    /** 
    * Maximum allowable cache size. 
    */ 
    private int maxSize; 


    /** 
    * Number of {@link #get(K)} queries for which appropriate {@code value} was found. 
    */ 
    private int keysFound; 


    /** 
    * Number of {@link #get(K)} queries for which appropriate {@code value} was not found. 
    */ 
    private int keysMissed; 


    /** 
    * Number {@code key-value} associations that were deleted from cache 
    * because of cash overflow. 
    */ 
    private int erasedCount; 


    /** 
    * Basic data structure LinkedHashMap provides 
    * convenient way for designing both types of cache: 
    * LRU and FIFO. Depending on its constructor parameters 
    * it can represent either of FIFO or LRU HashMap. 
    */ 
    private LinkedHashMap <K, V> entries; 


    /** 
    * If {@code type} variable equals {@code true} 
    * then LinkedHashMap will represent LRU HashMap. 
    * And it will represent FIFO HashMap otherwise. 
    */ 
    public FixedSizeCache(int maxSize, boolean type) { 

     if (maxSize <= 0) { 
      throw new IllegalArgumentException("int maxSize parameter must be greater than 0"); 
     } 

     this.maxSize = maxSize; 
     this.entries = new LinkedHashMap<K, V> (0, 0.75f, type); 
    } 


    /** 
    * Method deletes {@code key-value} associations 
    * until current cache size {@link #currentSize} will become 
    * less than or equal to maximum allowable 
    * cache size {@link #maxSize} 
    */ 
    private void relaxSize() { 

     while (currentSize > maxSize) { 

      // The strategy for choosing entry with the lowest precedence 
      // depends on {@code type} variable that was used to create {@link #entries} variable. 
      // If it was created with constructor LinkedHashMap(int size,double loadfactor, boolean type) 
      // with {@code type} equaled to {@code true} then variable {@link #entries} represents 
      // LRU LinkedHashMap and iterator of its entrySet will return elements in order 
      // from least recently used to the most recently used. 
      // Otherwise, if {@code type} equaled to {@code false} then {@link #entries} represents 
      // FIFO LinkedHashMap and iterator will return its entrySet elements in FIFO order - 
      // from oldest in the cache to the most recently added. 

      Map.Entry <K, V> entryToDelete = entries.entrySet().iterator().next(); 

      if (entryToDelete == null) { 
       throw new IllegalStateException(" Implemented method int getSize(K key, V value) " + 
         " returns different results for the same arguments."); 
      } 

      entries.remove(entryToDelete.getKey()); 
      currentSize -= getAssociationSize(entryToDelete.getKey(), entryToDelete.getValue()); 
      erasedCount++; 
     } 

     if (currentSize < 0) { 
      throw new IllegalStateException(" Implemented method int getSize(K key, V value) " + 
        " returns different results for the same arguments."); 
     } 
    } 


    /** 
    * All inheritors must implement this method 
    * which evaluates the weight of key-value association. 
    * Sum of weights of all key-value association in the cache 
    * equals to {@link #currentSize}. 
    * But developer must ensure that 
    * implementation will satisfy two conditions: 
    * <br>1) method always returns non negative integers; 
    * <br>2) for every two pairs {@code key-value} and {@code key_1-value_1} 
    * if {@code key.equals(key_1)} and {@code value.equals(value_1)} then 
    * {@code getSize(key, value)==getSize(key_1, value_1)}; 
    * <br> Otherwise cache can work incorrectly. 
    */ 
    protected abstract int getSize(K key, V value); 


    /** 
    * Helps to detect if the implementation of {@link #getSize(K, V)} method 
    * can return negative values. 
    */ 
    private int getAssociationSize(K key, V value) { 

     int entrySize = getSize(key, value); 

     if (entrySize < 0) { 
      throw new IllegalStateException("int getSize(K key, V value) method implementation is invalid. It returned negative value."); 
     } 

     return entrySize; 
    } 


    /** 
    * Returns the {@code value} corresponding to {@code key} or 
    * {@code null} if {@code key} is not present in the cache. 
    * Increases {@link #keysFound} if finds a corresponding {@code value} 
    * or increases {@link #keysMissed} otherwise. 
    */ 
    public synchronized final V get(K key) { 

     if (key == null) { 
      throw new NullPointerException("K key is null"); 
     } 

     V value = entries.get(key); 
     if (value != null) { 
      keysFound++; 
      return value; 
     } 

     keysMissed++; 
     return value; 
    } 


    /** 
    * Removes the {@code key-value} association, if any, with the 
    * given {@code key}; returns the {@code value} with which it 
    * was associated, or {@code null}. 
    */ 
    public synchronized final V remove(K key) { 

     if (key == null) { 
      throw new NullPointerException("K key is null"); 
     } 

     V value = entries.remove(key); 

     // if appropriate value was present in the cache than decrease 
     // current size of cache 

     if (value != null) { 
      currentSize -= getAssociationSize(key, value); 
     } 

     return value; 
    } 


    /** 
    * Adds or replaces a {@code key-value} association. 
    * Returns the old {@code value} if the 
    * {@code key} was present; otherwise returns {@code null}. 
    * If after insertion of a {@code key-value} association 
    * to cache its size becomes greater than 
    * maximum allowable cache size then it calls {@link #relaxSize()} method which 
    * releases needed free space. 
    */ 
    public synchronized final V put(K key, V value) { 

     if (key == null || value == null) { 
      throw new NullPointerException("K key is null or V value is null"); 
     } 

     currentSize += getAssociationSize(key, value);  
     value = entries.put(key, value); 

     // if key was not present then decrease cache size 

     if (value != null) { 
      currentSize -= getAssociationSize(key, value); 
     } 

     // if cache size with new entry is greater 
     // than maximum allowable cache size 
     // then get some free space 

     if (currentSize > maxSize) { 
      relaxSize(); 
     } 

     return value; 
    } 


    /** 
    * Returns current size of cache. 
    */ 
    public synchronized int currentSize() { 
     return currentSize; 
    } 


    /** 
    * Returns maximum allowable cache size. 
    */ 
    public synchronized int maxSize() { 
     return maxSize; 
    } 


    /** 
    * Returns number of {@code key-value} associations that were deleted 
    * because of cache overflow. 
    */ 
    public synchronized int erasedCount() { 
     return erasedCount; 
    } 


    /** 
    * Number of {@link #get(K)} queries for which appropriate {@code value} was found. 
    */ 
    public synchronized int keysFoundCount() { 
     return keysFound; 
    } 


    /** 
    * Number of {@link #get(K)} queries for which appropriate {@code value} was not found. 
    */ 
    public synchronized int keysMissedCount() { 
     return keysMissed; 
    } 


    /** 
    * Removes all {@code key-value} associations 
    * from the cache. And turns {@link #currentSize}, 
    * {@link #keysFound}, {@link #keysMissed} to {@code zero}. 
    */ 
    public synchronized void clear() { 
     entries.clear(); 
     currentSize = 0; 
     keysMissed = 0; 
     keysFound = 0; 
     erasedCount = 0; 
    } 


    /** 
    * Returns a copy of {@link #entries} 
    * that has the same content. 
    */ 
    public synchronized LinkedHashMap<K, V> getCopy() { 
     return new LinkedHashMap<K, V> (entries); 
    } 
} 

这个实现是很慢(因为同步的),如果我们有多个线程试图打电话让说get()方法。有没有更好的办法?

+1

你可以使用['ConcurrentHashMap'(http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/ConcurrentHashMap.html)呢? – 2013-04-11 14:52:58

+1

如果你还没有测量它,你只是猜测。你能提供任何数字来证明它太慢吗?你需要多快? – 2013-04-11 15:04:37

+1

你确定LHM是问题吗?看起来你周围添加的代码要慢得多。 – 2013-04-11 15:09:03

回答

5

我不知道这是有益的,但如果你能LinkedHashMapConcurrentHashMap取代,那么你会提高你的吞吐量 - 一个ConcurrentHashMap采用分片允许多个同步读者和作者。它也是线程安全的,所以你不需要同步你的读者和作者。

除此之外,将​​关键字的使用替换为ReadWriteLock。这将允许多个同时读取。

+0

java中的标准R/W锁很糟糕,因为它在元数据和展览共享中写入,所以性能不是很好。您可以使用LHM w/accessorder和R/W锁,因为读取操作会改变地图。 – bestsss 2013-04-11 15:46:32

4

尽量不要重复实现:Guava Caches。它几乎具有您需要的所有功能:基于大小的驱逐,并发,加权。如果它适合您的需求使用它。如果不是尝试实施你的,但总是首先评估(在我看来)。只是一个建议。

+0

LinkedHashMap是建立,但番石榴不是。如何使用LHM重新实现? – 2013-04-11 15:06:34

+0

他不是只用一个裸露的LHM,而是一个包装,如果必须选择,我会用番石榴而不是重新实现缓存。 – 2013-04-11 15:15:57

+0

我同意,但他添加的代码,他将不得不添加,因为番石榴也不看对象的大小。 – 2013-04-11 15:20:11

1

您需要在我的2.5GHz的酷睿i5笔记本电脑上运行这样

Map<Object, Object> map = Collections.synchronizedMap(new LinkedHashMap<Object, Object>(16, 0.7f, true) { 
    @Override 
    protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) { 
     return size() > 1000; 
    } 
}); 
Integer[] values = new Integer[10000]; 
for (int i = 0; i < values.length; i++) 
    values[i] = i; 

long start = System.nanoTime(); 
for (int i = 0; i < 1000; i++) { 
    for (int j = 0; j < values.length; j++) { 
     map.get(values[j]); 
     map.get(values[j/2]); 
     map.get(values[j/3]); 
     map.get(values[j/4]); 
     map.put(values[j], values[j]); 
    } 
} 
long time = System.nanoTime() - start; 
long rate = (5 * values.length * 1000) * 1000000000L/time; 
System.out.printf("Performed get/put operations at a rate of %,d per second%n", rate); 

打印性能测试

Performed get/put operations at a rate of 27,170,035 per second 

每秒多少百万次操作,你需要什么?

+0

您没有测试争用(这是它伤害严重的地方),并且您拥有最便宜的hashCode,特别是equals。锁将被偏向(或在测试中完全移除)。不敢说:但微不足道。 R/W锁不会更好,不能用于访问命令。 – bestsss 2013-04-11 15:44:08

+0

它不是测试很多东西,但如果你只整天访问同一张地图,你应该只使用一个线程。多线程只会变慢。只有OP是一个真实的用例。我的观点是你必须测试你的场景,以便了解性能会是什么样子。 – 2013-04-11 15:55:38

0

LRU方案在其自己的权利中涉及独占修改共享结构。所以争论就已经发生了,没有什么可以做的了。

如果您不需要严格的LRU并且可以容忍驱逐策略的某些不一致性,那么这些东西会查找并且更加明亮。您的条目(值包装)需要一些使用情况统计信息,并且您需要一些基于上述使用情况统计信息的到期策略。

然后,您可以使用基于ConcurrentSkipListMap的LRU相似结构(即您可以将其视为数据库索引),当缓存即将过期时,使用该索引并根据它过期元素。你将需要双重检查等,但它是可行的。 更新索引是免费的,但规模相当。请记住,ConcurrentSkipListMap.size()是一个昂贵的操作O(n),所以你不应该依赖。实现并不难,但不是小事或者,除非你有足够的同步争(芯)(LHM)可能是最简单的方法。

1

如前所述,问题的主要原因是更新LRU算法中的共享数据结构。为了克服这个问题,你可以使用分区,或者使用另一个驱逐算法,然后使用LRU。现代算法的执行效率比LRU好。请参阅我在cache2k benchmarks page上关于该主题的比较。

的cache2k驱逐实现的时钟和-PRO具有完全的读并发无锁。