LinkedHashMap源码分析及实现LRU
概述
从名字上看LinkedHashMap相比于HashMap,显然多了链表的实现。从功能上看,LinkedHashMap有序,HashMap无序。这里的顺序指的是添加顺序或者访问顺序。
基本使用
@Test
public void test1(){
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put(1,1);
map.put(3,3);
map.put(4,4);
map.put(2,2);
for (Map.Entry entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
map.get(1);
map.get(2);
System.out.println();
for (Map.Entry entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
我们通过构造函数的第三个参数accessOrder设为true(默认为false),该参数为true,则根据访问顺序排序。如下,当我们调用get()方法的时候,再次遍历发现数据被放到了最后面。
1:1
3:3
4:4
2:2
3:3
4:4
1:1
2:2
源码分析
首先LinkedHashMap继承了HashMap,因此其基本使用与HashMap几乎完全一致。
可以看到内部实现了双向链表 head指向最久访问,tail指向最新访问。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // Entry类维护了双线链表所需的前后节点指向
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
既然有了双向链表的基本定义,那么他是怎么实现双向链表的呢?找了这个类,发现并没有重写put方法,说明直接使用父类的put。那么直接看HashMap的putVal()方法。HashMap的源码就不再详细讲了,感兴趣的朋友可以看看我的另一篇文章。
观察HashMap的putVal的时候我们发现创建节点会频繁的使用到newNode()方法。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //添加节点的时候使用newNode
else {
......省略
我们观察LinkedHashMap,果然,在这里重写了,将每个节点添加到双向链表中。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
前面已经看到了put操作,LinkedHashMap在put方法执行的时候维护了一个双向链表。接下来我们看一下get操作。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null) // 调用父类的getNode
return null;
if (accessOrder) // accessOrder为true则调整双向链表顺序
afterNodeAccess(e);
return e.value;
}
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) { // accessOrder为true且 尾节点不为当前元素
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<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;
}
}
当我们将accessOrder设为true的时候,LinkedHashMap会将当前元素调整到双向链表末尾。
LRU缓存的实现
LRU(Least Recently Used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
根据定义,为了实现LRU,LinkedHashMap我们需要将accessOrder设为true。以保证最新使用的可以调整到链表的末尾。那么另一个比较重要的就是当缓存满的时候,需要淘汰最久没有使用的数据,及链表的头。
因此,该操作应该是存在put方法执行的过程中,由于LinkedHashMap并没有实现put方法,那么继续看HashMap里面的putVal(); 在方法快结束的时候发现了一个有趣的类,从方法名就感觉可以从这里入手。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //最后一个参数即下面的evict为true
}
afterNodeInsertion(evict); // HashMap并没有对这个类进行实现。
查看LinkedHashMap的afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) { // 我们只需要在这里重写移除最久没有使用的节点的判断条件,就可以移除节点了
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
我们假设LRU里最大容量为16,只需要将构造方法里的accessOrder设为true,并且重写removeEldestEntry的判断条件就可以了。
public class LruLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private Integer capacity;
public LruLinkedHashMap(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
public static void main(String[] args) {
LruLinkedHashMap<Integer, Integer> lru = new LruLinkedHashMap(16);
for (int i = 0; i < 16; i++) {
lru.put(i, i);
}
System.out.println("当前lru缓存的元素为");
for (Map.Entry entry : lru.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
lru.get(0);
lru.get(1);
System.out.println("当前lru缓存的元素为");
for (Map.Entry entry : lru.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
lru.put(16, 16);
lru.put(17, 17);
System.out.println("当前lru缓存的元素为");
for (Map.Entry entry : lru.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}
测试结果如下,最左边的为最近最久未使用。
当前lru缓存的元素为
0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9 10:10 11:11 12:12 13:13 14:14 15:15
当前lru缓存的元素为
2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9 10:10 11:11 12:12 13:13 14:14 15:15 0:0 1:1
当前lru缓存的元素为
4:4 5:5 6:6 7:7 8:8 9:9 10:10 11:11 12:12 13:13 14:14 15:15 0:0 1:1 16:16 17:17
LinkedHashMap源码分析及实现LRU的更多相关文章
- Java集合系列[4]----LinkedHashMap源码分析
这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHas ...
- LinkedHashMap源码分析
hashMap源码分析:hashMap源码分析 版本说明:jdk1.7LinkedHashMap继承于HashMap,是一个有序的Map接口的实现.有序指的是元素可以按照一定的顺序排列,比如元素的插入 ...
- Java 容器 LinkedHashMap源码分析1
同 HashMap 一样,LinkedHashMap 也是对 Map 接口的一种基于链表和哈希表的实现.实际上, LinkedHashMap 是 HashMap 的子类,其扩展了 HashMap 增加 ...
- 死磕 java集合之LinkedHashMap源码分析
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问 ...
- java Linkedhashmap源码分析
LinkedHashMap类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是插入次序,或者是最近最少使用(LRU)的次序.只比HashMap慢一点:而在迭代访问时反而更快,因为它使用链表维 ...
- Java8集合框架——LinkedHashMap源码分析
本文的结构如下: 一.LinkedHashMap 的 Javadoc 文档注释和简要说明 二.LinkedHashMap 的内部实现:一些扩展属性和构造函数 三.LinkedHashMap 的 put ...
- Java集合之LinkedHashMap源码分析
概述 HashMap是无序的, 即put的顺序与遍历顺序不保证一样. LinkedHashMap是HashMap的一个子类, 它通过重写父类的相关方法, 实现自己的功能. 它保留插入的顺序. 如果需要 ...
- LinkedHashMap 源码分析
LinkedHashMap LinkedHashMap 能解决什么问题?什么时候使用 LinkedHashMap? 1)LinkedHashMap 按照键值对的插入顺序进行遍历,LinkedHashM ...
- HashMap LinkedHashMap源码分析笔记
MapClassDiagram
随机推荐
- vue 导出xlsx表功能
详细步骤: 1.需要安装三个依赖: npm install -S file-saver xlsx npm install -D script-loader 两个命令行包含三个依赖. 2.项目中src下 ...
- Linux(CentOS7)下如何配置多个JDK环境变量
一.Linux版本 二.复制粘贴多个JDK出来,如下 cp -R jdk1.7.0_80/ jdk1.7.0_80-2 cp -R jdk1.7.0_80/ jdk1.7.0_80-3 三.配置多个J ...
- LeetCode算法题-1-bit and 2-bit Characters(Java实现)
这是悦乐书的第302次更新,第321篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第170题(顺位题号是717).有两个特殊字符,第一个字符可以用一个比特0表示,第二个字 ...
- 算法学习之BFS、DFS入门
算法学习之BFS.DFS入门 0x1 问题描述 迷宫的最短路径 给定一个大小为N*M的迷宫.迷宫由通道和墙壁组成,每一步可以向相邻的上下左右四格的通道移动.请求出从起点到终点所需的最小步数.如果不能到 ...
- Java 7 和 Java 8 中的 HashMap原理解析
HashMap 可能是面试的时候必问的题目了,面试官为什么都偏爱拿这个问应聘者?因为 HashMap 它的设计结构和原理比较有意思,它既可以考初学者对 Java 集合的了解又可以深度的发现应聘者的数据 ...
- 面试中被问Spring循环依赖的三种方式!!!
什么是循环依赖? 循环依赖其实就是循环引用,也就是两个或则两个以上的 Bean 互相持有对方,最终形成闭环.比如A依赖于B,B依赖于C,C又依赖于A.如下图: 如果在日常开发中我们用new 对象的方式 ...
- Idea中最最常见的快捷键
掌握如下快捷键,基本就够用了.没必要记那么多. Ø 命令:Ctrl+Shift+A可以查找所有Intellij的命令,并且每个命令后面还有其快捷键.所以它不仅是一大神键,也是查找学习快捷键的工具. ...
- JMC监控(Windows上远程连接监控Linux服务器的JVM)
Windows上远程连接监控Linux服务器的JVM:1.Linux服务器上配置:在Tomcat的tomcat-wms/bin/catalina.sh中添加CATALINA_OPTS="-X ...
- 前端之DOM操作
一.概念 javascript javascript是一种脚本语言,可以被浏览器解析,所以它可以称之为前端的三把利器之一. javascript跟java没有半毛钱关系. 声明局部变量:使用关键字va ...
- 剑指Offer面试题 二维数组中的查找
题目链接:https://www.nowcoder.com/questionTerminal/abc3fe2ce8e146608e868a70efebf62e 题目大意: 略 分析: 对萌醒很开拓思维 ...