您的位置:首页 > 移动开发 > Android开发

LRUCache原理及HashMap LinkedHashMap内部实现原理

2015-11-27 17:11 555 查看

LRUCache概述

在开发android应用时,加载图片我们都会加上二级缓存,一级是内存缓存,二级是硬盘缓存。在内存缓存中我们一般采用SDK提供给我们的LRUCahce。

LRU(least recently uses最近最少使用)是一种页面置换算法,最近最少使用的被替换掉。LRU算法是以时间轴为依据进行替换,而不是使用频率为依据替换。

常用的页面置换算法还有:

LFU(least frequently used 最不经常使用)是以单位时间内使用频率为依据进行替换,使用频率低的被替换掉。

FIFO(first in fist out 先进先出)按顺序进行替换。

LRUCache实现原理

先看一下LruCache的构造方法

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


可以看到LruCache内部维护了一个LinkedHashMap,LinkedHashMap继承自HashMap,所以我们先来研究下HashMap的实现原理。

HashMap的实现原理

还是先看下构造方法

public HashMap(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("Capacity: " + capacity);
}

if (capacity == 0) {
@SuppressWarnings("unchecked")
HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
table = tab;
threshold = -1; // Forces first put() to replace EMPTY_TABLE
return;
}

if (capacity < MINIMUM_CAPACITY) {
capacity = MINIMUM_CAPACITY;
} else if (capacity > MAXIMUM_CAPACITY) {
capacity = MAXIMUM_CAPACITY;
} else {
capacity = Collections.roundUpToPowerOfTwo(capacity);
}
makeTable(capacity);
}


其中
Collections.roundUpToPowerOfTwo(capacity);
方法是得到一个不小于所给数的2^n的数,就是说假如传入参数10,那么返回不小于10的2的n次方数就是16。具体的可以看这里

看下
makeTable
方法

private HashMapEntry<K, V>[] makeTable(int newCapacity) {
@SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable
= (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
table = newTable;
threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
return newTable;
}


可以看到,
HashMap
的构造方法中生成了一个大小为2^n的
HashMapEntry
的数组。我们看下
HashMapEntry
的结构

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


其中有
map
中的
key、value
还有根据
key
得到的
hash
值和它的后继引用
next
。其中
key value
都是
Object


既然是基于
Map
的,那么我们看下
HashMap
put
方法和
get
方法:

@Override public V put(K key, V value) {
if (key == null) {
//如果key为null调用该方法处理,需要注意的是HashMap允许key和value为null
return putValueForNullKey(value);
}
//根据key得到hash值
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
//根据hash值和内部维护数组的长度得到索引
int index = hash & (tab.length - 1);
//如果index处的e不为null,通过循环不断遍历e的下一个元素
for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
// 找到指定 key 与需要放入的 key 相等(hash 值相同
// 并且key的值相等)说明map中已经存在该数据,那么执行替换
if (e.hash == hash && key.equals(e.key)) {
//该方法是空实现,注意LinkedHashMap中会用到
preModify(e);
V oldValue = e.value;
e.value = value;
return oldValue;
}
}

// No entry for (non-null) key is present; create one
//index处的Entry为null,说明此处还没有数据Entry
modCount++;
if (size++ > threshold) {
//大于数组的长度,扩容 大小x2
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}


需要说明的是这句

int index = hash & (tab.length - 1);


首先
HashMap
采用一种所谓的
Hash 算法
来决定每个元素的存储位置。 Java中
Object
提供一个
hashCode()
方法,也就是说每个对象都有
hashCode()
方法得到一个唯一的
hash值


上面得到index的方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置——而 HashMap 底层数组的长度总是 2 的 n 次方,当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。

也就是说该方法返回的值
index
总是在0–table.length之间循环。也就是说虽然key的hash值不同,但是他们在
HashMap
中的存储位置是一样的。你可能有点疑惑,我们马上就会找到答案。

那么
put
方法会把
key-value
放入
HashMap
,首先根据
key
hash
值得到在
HashMap
中的存储位置,如果该位置上有数据,并且key的值相等,那么直接执行修改
value
的操作。如果不想的会执行
addNewEntry(key, value, hash, index);
方法。这个方法是干什么的呢,我们来看下

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


代码很简单,生成一个新的
Entry
,并且把
key、value、hash
相应赋值,然后最重要的是把
HashMap
index
位置的
Entry
的引用付给了新
Entry
next
变量。也就是说在这里形成了一个Entry链,并且新的
Entry
位于
Entry
链的头部,然后把
index
的位置上的值更改成了新生成的
Entry


HashMap
get
方法跟
put
方法的逻辑基本是一致的,感兴趣的读者可以自己查阅。

那么分析到这里我们也就理解了
HashMap
的基本结构,用一张图表示



可以看到
HashMap
中维护了一个数组,并且先用数组进行定位查找,然后用链表维护数组中同一位置上的
Entry
。所以说
HashMap
效率高不是没有道理的。

LinkedHashMap的实现原理

同样的先看下构造方法:

public LinkedHashMap(
int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
header = new LinkedEntry<K, V>();
this.accessOrder = accessOrder;
}


在LruCache中是这样使用的:

this.map = new LinkedHashMap<K, V>(0, 0.75f, true);


注意
accessOrder
的值为true,

首先调用父类也就是
HashMap
的构造方法生成一个数组,然后new了一个
LinkedEntry
做为
header
,看下
LinkedEntry
结构

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


可以看到它继承自
HashMap
中的
HashMapEntry
,那么其中的数据就有: key、value、hash、next、nxt、prv。

可以看到
LinkedHashMap
HashMap
的基础上又增加了双向链表。也就是说LinkedHashMap的结构是数组+单向链表+双向链表。

那么这个双向链表是做什么用的呢。我们继续分析。

还记得
HashMap
中的
preModify
方法吗,
LinkedHashMap
实现了该方法用于
put
时替换
index
位置已经存在并且
key
值相等的节点:

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


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;
e.nxt = header;
e.prv = oldTail;
oldTail.nxt = header.prv = e;
modCount++;
}


可以看到首先把e从双向链表中移除,然后放到链表的最尾端。可以看到其中的链表不仅是双向的而且是循环的。联系到
HashMap
中的
put
中的的操作,这一步只是修改
value
值并且更改在双向循环链表中的位置。

再看下
LinkedHashMap
中的
addNewEntry
方法:

@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry<K, V> header = this.header;

// Remove eldest entry if instructed to do so.
LinkedEntry<K, V> eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}

// 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;
}


做的事情是生成一个新的
LinkedEntry
,把它的引用放到双向循环列表的尾部,并且把它的引用赋值到数组中相应位置上的值。

再看下
get
方法

@Override public V get(Object key) {
/*
* This method is overridden to eliminate the need for a polymorphic
* invocation in superclass at the expense of code duplication.
*/
if (key == null) {
HashMapEntry<K, V> e = entryForNullKey;
if (e == null)
return null;
if (accessOrder)
makeTail((LinkedEntry<K, V>) e);
return e.value;
}

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


逻辑跟
HashMap
基本一样,根据
index
在数组中定位,然后在单向列表中查找。也就是不管我们在
LinkedHashMap
中执行
get
方法或者
put
方法,最后操作的那个
entry
都是插到双向链表的尾部。那么头部的
nxt
的那个
entry
也就是最久未使用的
entry
依据这个特性我们就可以实现
LRU
实现缓存。

依据看一张图



然而分析道这里实际上并没有用到双向循环列表,那么这个双向循环列表到底是干什么用的呢?

我们看下LruCache中的put方法

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


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 || map.isEmpty()) {
break;
}

Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}

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


最后如果当前大小大于我们设定的最大缓存大小那么执行
trimToSize
方法中根据
LinkedHashMap
entrySet
得到
iterator
,然后执行
next
方法,最后执行的是LinkedHashMap中的这些代码

LinkedEntry<K, V> next = header.nxt;
LinkedEntry<K, V> lastReturned = null;
int expectedModCount = modCount;

public final boolean hasNext() {
return next != header;
}

final LinkedEntry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedEntry<K, V> e = next;
if (e == header)
throw new NoSuchElementException();
next = e.nxt;
return lastReturned = e;
}


可以看到返回的是header.nxt,也就是双向循环链表中的第一个entry,那么也就是最久未使用的那个
entry


总结

到此LruCache的基本原理我们都分析清楚了,并且梳理了HashMap和LinkedHashMap的内部实现原理,虽然我们在开发项目的时候对基本数据结构的内部原理不太关注,但是我们也应该学习其内部原理。毕竟基础才是最重要的。说到HashMap又想到了androidSDK中的SparseArray可以替代key为Integer类型的HashMap,感性却的可以看这篇文章
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android hashmap 缓存