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实现了最少用最先淘汰算法。