Android基础知识-LRU算法相关数据结构

https://blog.csdn.net/justloveyou_/article/details/71713781

一、LinkedHashMap原理

它是一个将所有Entry节点链入一个双向链表的HashMap
由于 LinkedHashMap 是HashMap的子类,所以LinkedHashMap自然会【拥有HashMap的所有特性】。

它额外定义了【一个以 head 为头节点的双向链表 用于保持迭代顺序

默认 按插入顺序排序

对于每次 put 进来 Entry,除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部
head 节点不包含数据


LruCache 是一个泛型类,内部采用一个 LinkedHashMap 以 强引用的方式存储外界的缓存对象
LruCache 会 移除较早使用的缓存对象,然后再添加新的缓存对象
LruCache 是线程安全的,内部存取数据都用了同步

LinkedHashMap 为什么使用双向链表,而不是单链表?

LinkedHashMap 的重要运用之一是,实现 LRU算法,每次被访问的元素,都会被移到双向链表尾部。
使用双向链表,移动元素速度快,只要把前后节点相连、把本节点放到链表末尾即可。
如果使用单链表,需要先遍历找到 当前节点的上一个节点。这个查找过程会比较耗时。

LinkedHashMap,等于是在HashMap 的哈希表中,用一条线,按插入/访问顺序把各个元素串起来。

LRU算法实现

accessOrder 为 true时,按访问时间排序。
每次访问一个节点后,都会把该节点移到双向链表末端
LinkedHashMap 的遍历,遍历的是 保存顺序的双链表,从头节点往尾部遍历(即从最久未访问的元素开始遍历)
LruCache 每次插入元素,如果容量达到上限,就会遍历删除,删除的就是最久未访问的元素

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	
	/* 一个数组,数组下标由key 的hash值计算而来
	 *
	 * 数组中的每个元素,都是单链表的头节点/jdk1.7以后,也可能是二叉树的根节点
	 *  [Node_hash1, Node_hash2, Node_hash3, ...] 【数组】
	 *      |			  |			 |
	 *  Node_hash1_2  Node_hash2_2  ...           【链表】
	 *		|             |
	 *  Node_hash1_3     ...
	 */
	transient Node<K,V>[] table;
}

/* 每次put进来Entry,除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部。
 * 通过双向链表来保存插入顺序。
 */
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
	
	//头节点,也是双链表中最老的元素
	transient LinkedHashMapEntry<K,V> head;

	//尾节点,双链表中最新的元素
	transient LinkedHashMapEntry<K,V> tail;
	
	/* true表示按照访问顺序迭代,
	 * false时表示按照插入顺序 
	 *
	 * true时:会在每次访问(put/get)时,把最近访问的元素放到双链表的尾部,从而实现按访问时间排序。
	 */
	private final boolean accessOrder; 

	/* LinkedHashMap的Entry,在HashMap.Node基础上,增加【before、after】
	 * 用于与双向链表相连,从而维护Entry插入的先后顺序
	 */
	static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
		LinkedHashMapEntry<K,V> before, after;
		LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
			super(hash, key, value, next);
		}
	}
	
	//构造函数最后会调用该方法,用于初始化双向链表
	void init() {
		//头节点,不包含数据
		header = new Entry<K,V>(-1, null, null, null);
		header.before = header.after = header;
	}		
	
	public V get(Object key) {
		Node<K,V> e;
		if ((e = getNode(hash(key), key)) == null)
			return null;
		if (accessOrder)//按访问时间排序
			afterNodeAccess(e);//把最近访问的元素移到链表尾部
		return e.value;
	}
	
	//将节点移到链表尾部
	void afterNodeAccess(Node<K,V> e) { // move node to last
		LinkedHashMapEntry<K,V> last;
		if (accessOrder && (last = tail) != e) {
			//p 指向我们要移动的节点
			LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
			p.after = null;
			if (b == null)
				head = a;
			else
				b.after = a;
			if (a != null)
				a.before = b;
			else
				last = b;
			if (last == null)
				head = p;
			else {
				p.before = last;
				last.after = p;
			}
			tail = p;
			++modCount;
		}
	}
	
	/**
	 * LinkedHashMap 的 entrySet()遍历,执行的是 LinkedEntrySet.foreach()方法
	 */ 
	final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
		...			
		public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
			if (action == null)
				throw new NullPointerException();
			int mc = modCount;
			//遍历的是 维护的双链表,从头节点往尾部遍历
			for (LinkedHashMapEntry<K,V> e = head; (e != null && mc == modCount); e = e.after)
				action.accept(e);
			if (modCount != mc)
				throw new ConcurrentModificationException();
		}
	}
}
二、LruCache 原理
LRU(Least Recently Used) 最近最少使用

public class LruCache<K, V> {
	private final LinkedHashMap<K, V> map;//内部就是一个LinkedHashMap

	/** Size of this cache in units. Not necessarily the number of elements. */
	private int size;
	private int maxSize;
	...
	
	public LruCache(int maxSize) {
		if (maxSize <= 0) {
			throw new IllegalArgumentException("maxSize <= 0");
		}
		this.maxSize = maxSize;
		/**
		 * 最后一个元素 true,表示:LinkedHashMap 【按访问时间排序,最近访问的放尾部】
		 */
		this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
	}
	
	/**
	 * 获取缓存数据
	 */
	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++;
		}
		...
	}

	/**
	 * 存入数据,会把存入的数据,塞到 LinkedHashMap 的头节点
	 */
	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;
	}
	
	//调整缓存大小(关键方法)
	private void trimToSize(int maxSize) {
		while (true) {
			K key;
			V value;
			synchronized (this) {
				...
				if (size <= maxSize) {
					break;
				}
				/* 容量达到上限时,遍历 LinkedHashMap,LinkedHashMap每次访问元素都会把元素移动到链表尾部
				 * 所以 LinkedHashMap 头部的元素是最久没有访问的。
				 *
				 * 这里entrySet()可以看 LinkedEntrySet.forEach()是从head节点开始往后遍历的
				 * 这里从 head 节点开始,边遍历边删除,从最老的元素开始删除,直到容量不超过上限。
				 */
				Map.Entry<K, V> toEvict = null;
				//返回最近最久未使用的元素,也就是链表的表头元素
				for (Map.Entry<K, V> entry : map.entrySet()) {
					toEvict = entry;
				}
				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 以强引用的方式存储外界的缓存对象。

LruCache 会移除较早使用的缓存对象,然后再添加新的缓存对象。

LruCache 是线程安全的,内部存取数据都用了同步。

三、DiskLruCache 原理

DiskLruCache 也是通过LRU算法对缓存进行管理,通过LinkedHashMap,按访问时序排序。
每次访问元素,都会把元素移动到链表尾部。
head节点不包含数据。head的下一个节点就是包含数据的最久没有访问的元素。

当 LinkedHashMap 达到容量时,通过 LinkedHashMap.entrySet(),从链表头节点开始遍历,边遍历边删除。
直到容量不超过上限。

从而实现LRU算法。





推荐阅读:
《Android开发艺术探索》第12.2 Android中的缓存策略
Map 综述(二):彻头彻尾理解 LinkedHashMap https://blog.csdn.net/justloveyou_/article/details/71713781

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值