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
随机推荐
- go的生产者-消费者模式
package main import ( "fmt" "math/rand" "time" ) // 数据生产者 func produce ...
- GlusterFS群集存储项目
最小化安装的centos7.5 内存大于1GB 关闭selinux,防火墙端口放行(port:24007,111)(测试建议关闭firewalld) 一.环境部署 所有软件包离线安装,原因是yum安装 ...
- CentOS 6忘记root密码的解决办法
1.在开机启动的时候按键盘上的“E”键 或者“ESC”键,会进入如下界面 2.选择相应的内核,再次按“E”,出现下图,选择第二项,再次按“E”键 3.经过第二步,这个画面可以编辑,在信息的最后加“空格 ...
- vue项目报错webpackJsonp is not defined
在vue单页面应用中,我们大概都会使用CommonsChunkPlugin这个插件. 传送门 CommonsChunkPlugin 但是在项目经过本地测试没有任何问题,打包上线后却会报错 webpac ...
- 怪事年年有,今天特别多!org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'empno' not found. Available parameters are [emp, deptno, param1, param
错误: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.Binding ...
- MongoDB索引基本操作
一.简介 在MongoDB建立索引能提高查询效率,只需要扫描索引只存储的这个集合的一小部分,并只把这小部分加载到内存中,效率大大的提高,如果没有建立索引,在查询时,MongoDB必须执行全表扫描,在数 ...
- Linux下的crontab
Ubuntu服务器/var/log下没有cron日志,这里记录一下如何ubuntu server如何查看crontab日志 crontab记录日志 修改rsyslog sudo vim /etc/rs ...
- wget在linux中安装出现错误解决办法
在使用wget命令报错 certificate common name 'xxx' doesn't match requestde host name,我们一般的解决办法是查找下载地址,但是有时候更换 ...
- react中的this.setState()
修改组件的状态可以使用的一些方法: 1.比较常用的 this.setState({ message:"你好" }) 2.state更新是异步的时候 因为this.props和thi ...
- JS对JSON对象遍历输出的时候真的是按照顺序输出吗?
对象的遍历输出并不是按照对象属性定义顺序来的,那么是按照什么规则来的呢,仔细深入研究你会发现,这还跟浏览器有关系,Chrome跟IE是不一样的,所以给出以下结论: Chrome Opera 的 Jav ...