LinkedHashMap及LruCache是如何实现最少用最先淘汰算法

LinkedHashMap及LruCache是如何实现最少用最先淘汰算法

LinkedHashMap是链表的方式保存,它的最小单元是LinkedEntry。但是这些单元又是保存在一个hashtable里。

LinkedEntry

 /**
     * LinkedEntry adds nxt/prv double-links to plain HashMapEntry.
     */
    static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
        LinkedEntry<K, V> nxt;
        LinkedEntry<K, V> prv;

        /** Create the header entry */
        LinkedEntry() {
            super(null, null, 0, null);
            nxt = prv = this;
        }

        /** Create a normal entry */
        LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                    LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
            super(key, value, hash, next);
            this.nxt = nxt;
            this.prv = prv;
        }
    }

LinkedEntry的HashMapEntry的基础上加了一个pre,nxt来实现双向链表的数据结构

HashMapEntry

  static class HashMapEntry<K, V> implements Entry<K, V> {
        final K key;
        V value;
        final int hash;
        HashMapEntry<K, V> next;

        HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

HashMapEntry保存了key,value,hash,next

put方法与HashMap一样

 public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);

    //...ignore code

通过secondaryHash实现二次hash,不过看完也是一脸懵逼

    private static int secondaryHash(int h) {
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

然后通过二次hash去找hashtable要索引,这里面的索引用的是&,我们一般会通过hash%tab.length求余实现,但是这里通过&会更快

  HashMapEntry<K, V>[] tab = table;
  int index = hash & (tab.length - 1);

index与HashTable的长度

用&需要有前提的:table的长度是2的n次方;假如给定一个i它能求出>i的最小整数,这个整数是2的n次方

  public static int roundUpToPowerOfTwo(int i) {
        i--; // If input is a power of two, shift its high-order bit right.

        // "Smear" the high-order bit all the way to the right.
        i |= i >>>  1;
        i |= i >>>  2;
        i |= i >>>  4;
        i |= i >>>  8;
        i |= i >>> 16;

        return i + 1;
    }

put的时候找到该entry

然后通过int index = hash & (tab.length - 1);方法得到index,
如果length=8(十进制),hash=100(二进制), 那就是‭01100100‬&111得到的就是100,就会把这个entry放在第4个位置上。
得到index以后去访问第tab[index]元素,如果为空就跳出for;如果不为空,去取该entry的hash,key去比,如果找到了就替换,并返回以前的oldValue

for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
    if (e.hash == hash && key.equals(e.key)) {
        preModify(e);
        V oldValue = e.value;
        e.value = value;
        return oldValue;
    }
}

如果没找到该entry

modCount++;
当size>threshold(3/4的容量)就扩容,扩容的方式是两倍

// No entry for (non-null) key is present; create one
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        addNewEntry(key, value, hash, index);

HashMap 添加新entry的时候会把新的Entry放在第一个位置,也就是插入进去

  void addNewEntry(K key, V value, int hash, int index) {
        table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
    }
   HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

[4]=new->old也就是说,hash相同的两个不同节点,新的会在数组的第一个位置。

以上是HashMap的put,及addNewEntry;而LinkedHashMap对HashMap做了改进。

LinkedHashMap里有一个header永远都是Header
第一个header它的pre=nxt=自己

void init() {
    header = new LinkedEntry<K, V>();
    }
LinkedEntry() {
    super(null, null, 0, null);
    nxt = prv = this;
    }


LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
    super(key, value, hash, next);
    this.nxt = nxt;
    this.prv = prv;
    }
 @Override void addNewEntry(K key, V value, int hash, int index) {
        LinkedEntry<K, V> header = this.header;

       //...ignore code

        // Create new entry, link it on to list, and put it into table
        LinkedEntry<K, V> oldTail = header.prv;
        LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
                key, value, hash, table[index], header, oldTail);
        table[index] = oldTail.nxt = header.prv = newTail;
    }

我们来分析LinkedHashMap的addNewEntry;假如添加第一个entry叫1:

oldTail=head.pre —– oldtail=head

newTail=1; 1.nxt=head,1.pre=oldtail(head) 也就是head<-1->head

//oldTail.nxt = header.prv = newTail

head.next=head.pre=1也就是head<->1<->head

然后添加第二个entry叫2

oldTail=head.pre —— oldtail=1

new Tail=2 2.next=head 2.pre=oldtail(1) 也就是1<-2->head

//oldTail.nxt = header.prv = newTail

1.next=head.pre=2 也就是1->2<-head

最后head<->1<->2<->head

同理如果加入第三个entry叫3

最后会得到head<->1<->2<->3<->head形成一个环,也就是每次添加一个元素就放在链表的末尾。

LinkedHashMap有一个重要的函数叫makeTail

makeTail

假如e是1

    private void makeTail(LinkedEntry<K, V> e) {
        // Unlink e
        e.prv.nxt = e.nxt;
        e.nxt.prv = e.prv;
        //先把自己从链表结构里移除

        // Relink e as tail
        LinkedEntry<K, V> header = this.header;
        LinkedEntry<K, V> oldTail = header.prv;//oldTail=3
        //
        e.nxt = header;//1->head
        e.prv = oldTail;//3<-1
        oldTail.nxt = header.prv = e;//3->1<-header
        //综合起来head<->2<->3<->1<->header
        modCount++;
    }

由此可见makeTail(1)是把1放到了最末尾

eldest

 public Entry<K, V> eldest() {
        LinkedEntry<K, V> eldest = header.nxt;
        return eldest != header ? eldest : null;
    }

eldest方法把header.next返回也就是head<->2<->3<->1<->header中的2,也就链表的头部第二个元素(第一个是header),也就是说,eldest拿到的是最先添加的;

但是,如果makeTail会把某一个元素调到末尾,也就是说LinkedHashMap有返回最后添加的元素的能力,也有把某个元素移除插入成最新的元素,有了这个LruCache就好实现了。

LruCache里的LinkedHashMap

 public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
 public LinkedHashMap(
            int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        init();
        this.accessOrder = accessOrder;
    }

这里面注意一个变量accessOrder=true,这个标识为true代表它拥有makeTail的能力

当LinkedHashMap.put的时候,它其实是调用HashMap的put

public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
            if (e.hash == hash && key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }
//...ignore code
    }

注意preModify 方法在HashMap里是空实现,而在LinkedHashMap里有实现:

   @Override void preModify(HashMapEntry<K, V> e) {
        if (accessOrder) {
            makeTail((LinkedEntry<K, V>) e);
        }
    }

如果accessOrder为true的话就会把该Entry放到队尾。也就是说put的时候会把该Entry放到链表的末尾,eldest是从头部拿的。

而get方法也有makeTail()

 public V get(Object key) {

      //...ignore code

        int hash = Collections.secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
                e != null; e = e.next) {
            K eKey = e.key;
            if (eKey == key || (e.hash == hash && key.equals(eKey))) {
                if (accessOrder)
                    makeTail((LinkedEntry<K, V>) e);
                return e.value;
            }
        }
        return null;
    }

如果accessOrder就调用makeTail。

LruCache就是通过accessOrder=true能合理的算出哪个value最少使用,从而实现最少用优先淘汰的需求。

LruCache.put()

map去put一个value(此时LinkedHashMap会把该value放到链表的末尾),如果之前有,就返回一个previous,然后把previous移除,再重新走trimToSize()

 public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }

LruCache.get()

如果linkedHashMap.get(内部会把该Entry放到链表末尾)能拿到,hitCount++,然后返回,否则会调createValue,该方法返回空,留给子类实现,如果createValue不为空,则会put到linkedHashMap里

 public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }

        /*
         * Attempt to create a value. This may take a long time, and the map
         * may be different when create() returns. If a conflicting value was
         * added to the map while create() was working, we leave that value in
         * the map and release the created value.
         */

        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        synchronized (this) {
            createCount++;
            mapValue = map.put(key, createdValue);

            if (mapValue != null) {
                // There was a conflict so undo that last put
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }

每次有新的value被put都要调用一下trimToSize。

trimToSize()

trimToSize就是如果空间满了去链表里拿eldest删除它,直到空间够用为止

 public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize) {
                    break;
                }

                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

至此,LruCache巧妙的通过LinkedHashMap实现了最少用最先淘汰算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值