聊聊LRU

376 阅读4分钟
原文链接: mp.weixin.qq.com

什么是LRU

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

使用LinkedHashMap实现

思路:

1、新数据插入到链表头部;

2、每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

3、当链表满的时候,将链表尾部的数据丢弃。

代码实现

    import java.util.ArrayList;

    import java.util.Collection;

    import java.util.LinkedHashMap;

    import java.util.Map;

    import java.util.concurrent.locks.Lock;

    import java.util.concurrent.locks.ReentrantLock;

    //类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,具体参见JDK文档

    public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {

    private final int maxCapacity;

    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    private final Lock lock = new ReentrantLock();

    public LRULinkedHashMap(int maxCapacity) {

    super(maxCapacity, DEFAULT_LOAD_FACTOR, true);

    this.maxCapacity = maxCapacity;

    }

    @Override

    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {

    return super.removeEldestEntry(eldest);

    }

    @Override

    public boolean containsKey(Object key) {

    try {

    lock.lock();

    return super.containsKey(key);

    }finally {

    lock.unlock();

    }

    }

    @Override

    public V get(Object key) {

    try {

    lock.lock();

    return super.get(key);

    }finally {

    lock.unlock();

    }

    }

    @Override

    public V put(K key, V value) {

    try {

    lock.lock();

    return super.put(key, value);

    }finally {

    lock.unlock();

    }

    }

    @Override

    public int size() {

    try {

    lock.lock();

    return super.size();

    }finally {

    lock.unlock();

    }

    }

    @Override

    public void clear() {

    try {

    lock.lock();

    super.clear();

    }finally {

    lock.unlock();

    }

    }

    public Collection<Map.Entry<K, V>> getAll() {

    try {

    lock.lock();

    return new ArrayList<Map.Entry<K, V>>(super.entrySet());

    }finally {

    lock.unlock();

    }

    }

    }

分析

【命 中率】

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

【复杂度】实现简单。

【代价】 命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

HashMap 和 双向链表实现 LRU

整体的设计思路可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。

LRU 存储是基于双向链表实现的,下面的图演示了它的原理。 其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。

下面展示了,预设大小是 3 的,LRU存储的在存储和访问过程中的变化。 为了简化图复杂度,图中没有展示 HashMap部分的变化,仅仅演示了上图 LRU 双向链表的变化。我们对这个LRU缓存的操作序列如下:

save("key1", 7)

save("key2", 0)

save("key3", 1)

save("key4", 2)

get("key2")

save("key5", 3)

get("key2")

save("key6", 4)

相应的 LRU 双向链表部分变化如下:

总结一下核心操作的步骤:

1、save(key, value) ,首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。

2、get(key) ,通过 HashMap 找到 LRU 链表节点,因为根据LRU 原理,这个节点是最新访问的,所以要把节点插入到队头,然后返回缓存的值。

代码实现: 非线程安全,若实现安全,则在响应的方法加锁。(如果要线程安全,可将hashmap换成线程安全的hashtable等)

                                                                                                                                    

    import java. util. HashMap;

    public class LRUCache <K , V > {

    private int currentCacheSize ;

    private int CacheCapcity;

    private HashMap< K, CacheNode > caches ;

    private CacheNode first ;

    private CacheNode last ;

    public LRUCache( int size ) {

    currentCacheSize = 0;

    this. CacheCapcity = size ;

    caches = new HashMap< K, CacheNode>( size);

    }

    public void put (K k ,V v ){

    CacheNode node = caches .get (k );

    if( node == null ){

    if( caches. size() >= CacheCapcity ){

    caches .remove (last .key );

    removeLast ();

    }

    node = new CacheNode();

    node .key = k ;

    }

    node .value = v ;

    moveToFirst (node );

    caches .put (k , node );

    }

    public Object get (K k ){

    CacheNode node = caches .get (k );

    if( node == null ){

    return null;

    }

    moveToFirst (node );

    return node .value ;

    }

    public Object remove (K k ){

    CacheNode node = caches .get (k );

    if( node != null ){

    if( node. pre != null ){

    node .pre .next =node .next ;

    }

    if( node. next != null ){

    node .next .pre =node .pre ;

    }

    if( node == first){

    first = node .next ;

    }

    if( node == last){

    last = node .pre ;

    }

    }

    return caches .remove (k );

    }

    public void clear (){

    first = null;

    last = null;

    caches .clear ();

    }

    private void moveToFirst (CacheNode node){

    if( first == node){

    return;

    }

    if( node. next != null ){

    node .next .pre = node .pre ;

    }

    if( node. pre != null ){

    node .pre .next = node .next ;

    }

    if( node == last){

    last = last .pre ;

    }

    if( first == null || last == null ){

    first = last = node ;

    return;

    }

    node .next =first ;

    first .pre = node ;

    first = node ;

    first .pre =null ;

    }

    private void removeLast (){

    if( last != null ){

    last = last .pre ;

    if( last == null ){

    first = null;

    }else {

    last .next = null;

    }

    }

    }

    @Override

    public String toString (){

    StringBuilder sb = new StringBuilder();

    CacheNode node = first ;

    while( node != null ){

    sb .append (String .format ("%s:%s " , node .key ,node .value ));

    node = node .next ;

    }

    return sb .toString ();

    }

    class CacheNode{

    CacheNode pre ;

    CacheNode next ;

    Object key ;

    Object value ;

    public CacheNode(){

    }

    }

    public static void main (String [] args ) {

    LRUCache< Integer, String> lru = new LRUCache <Integer ,String >(3 );

    lru .put (1 , "a"); // 1:a

    System. out. println( lru. toString());

    lru .put (2 , "b"); // 2:b 1:a

    System. out. println( lru. toString());

    lru .put (3 , "c"); // 3:c 2:b 1:a

    System. out. println( lru. toString());

    lru .put (4 , "d"); // 4:d 3:c 2:b

    System. out. println( lru. toString());

    lru .put (1 , "aa"); // 1:aa 4:d 3:c

    System. out. println( lru. toString());

    lru .put (2 , "bb"); // 2:bb 1:aa 4:d

    System. out. println( lru. toString());

    lru .put (5 , "e"); // 5:e 2:bb 1:aa

    System. out. println( lru. toString());

    lru .get (1 ); // 1:aa 5:e 2:bb

    System. out. println( lru. toString());

    lru .remove (11 ); // 1:aa 5:e 2:bb

    System. out. println( lru. toString());

    lru .remove (1 ); //5:e 2:bb

    System. out. println( lru. toString());

    lru .put (1 , "aaa"); //1:aaa 5:e 2:bb

    System. out. println( lru. toString());

    }

    }

参考 文档:

https://zhuanlan.zhihu.com/p/34133067

https://www.jianshu.com/p/62e829c37adf

推荐阅读

1数据库知识整理

2、Spring系列面试题

3、Java内存模型(JMM)