ConcurrentHaspLRUHashMap实现初探

ConcurrentHaspLRUHashMap实现初探

一、 关于LRU。

LRU 即 Least Rencetly Used(最近最少使用)缓存替换策略。在任何LRU算法中,它必定有以下两个策略组成:

1、 退化 策略。根据访问情况,对节点按热度进行排序(hot->cold),以便决定哪些节点是热节点(hot)的,哪些节点是冷节点(cold)的。这个退化的策略,一般按以下两种方式去处理:

l 非集中式。即每命中一次就进行退化操作。

非集中式的退化操作,往往由双向链表的方式去实现。每次命中之后就移动命中节点在链表中的位置。(位置靠前的就是hot的数据)。当然,复杂的策略中,有用queue数组进行hot分级等。

l 集中式。定期去进行退化操作。

在集中式的退化操作,常用的策略是:每次命中之后,记录一个时间戳、定时器时间点等等参数。由一个线程去扫描,定期清除老数据。

2、 清除 策略。即去掉那些cold的数据。

l 替换。这个在操作系统缓存中应该是一个常用的做法。

l 删除。删除掉数据,以腾出空间放新的数据。(因为内存是有限的)

二、 ConcurrentHashMap与LinkedHashMap

在JAVA中,LRU的原生实现是JDK中LinkedHashMap。LinkedHashMap继承自HashMap

【实现原理】 简单说就是HashMap的每个节点做一个双向链表。每次访问这个节点,就把该节点移动到双向链表的头部。满了以后,就从链表的尾部删除。但是LinkedHashMap并是非线程安全(其实现中,双向链表的操作是没有任何线程安全的措施的)。

对于线程安全的HashMap,在JDK中有ConcurrentHashMap原生支持。

【实现原理】采用锁分离机制,把一个HashMap分成多个segement,对每个segement的写操作上锁。同时,他的get()操作是没有锁的,具体思想就是把每个hash槽中的链表的头节点置成final的。对hash槽中链表操作,只能从头部去处理。这样就不会有读不一致的情况出现。这个原理,最好还是看源码,比较清晰。

三、 ConcurrentLRUHashMap的实现方式一:直接包装LinkedHashMap。

即,在LinkedHashMap外层全部加锁。

典型代码:


对LinkedHashMap做包装,所有访问都是带锁委托给LinkedHashMap。这样虽然解决了多线程安全问题。但是,是以严重的性能消耗为代价代价。 ## 四、 ConcurrentLRUHashMap实现方式二:直接改造ConcurrentHashMap 该方案主要是重写ConcurrentHashMap。 1、 给每个Entry加一个timestamp。 2、 每次get命中的话,修改时间戳。 3、 定时统计整个map的总量,如果总量大于某个阈值,则deadline往后推。同时,在put的时候,检查hash槽里面每个节点的时间戳,如果已经过期,就删除掉过期节点。 上述做法,删除操作分布在每次put操作中。所以,删除效率比较高。但是,由于时间片不可控,最终将导致内存爆炸的情况出现。 请看下面一种场景: [![](http://rdc.taobao.com/team/jm/files/2011/01/1-205x300.png)](http://rdc.taobao.com/team/jm/files/2011/01/1.png) [![](http://rdc.taobao.com/team/jm/files/2011/01/2.png)](http://rdc.taobao.com/team/jm/files/2011/01/2.png) 横坐标表示一个时间片。面积表示这个时间片里面节点数量。 假定节点命中率为50%(命中后,更新到命中时刻的时间片),每个时间片写入10条新数据。 我们可以在运行过程中,每个时间片定义一个更新一次deadline。在put数据的时候,我们可以检查hash槽中Entry是否过期,如果已经过期,则删掉过期数据。 [![](http://rdc.taobao.com/team/jm/files/2011/01/3-300x293.png)](http://rdc.taobao.com/team/jm/files/2011/01/3.png) 对于deadline的计算,我们可以设置三个阈值(a<b<c) a) totalCount<a deadline不变 b) a<totalCount<b deadline=deadline+cycle c) b<totalCount<c deadline=deadline+2*cycle d) totalCount>c deadline=currentTime 上述看似非常优雅的方案,却隐藏几个严重的问题: 1、 时间片的选择问题。 这个方案中,时间片的选择是一个比较困难的问题。因为,如果系统在一个时间片之内爆掉内存的话,系统将直接崩溃。 当然,这个问题,我们可以加外部限制得方式去控制 2、 deadline 之前的数据,不能很快删除。导致deaddata滞留,浪费大量的内存 假定 deadline之前的数据,约为总数据量的10%。因为删数据操作,只在put的时候。假定每个时间点的put操作,能覆盖20%的hash槽。这个10%*20%=2%,每个时间点,只能删除2%的过期数据。然后,随着时间的推移。这个过程必将趋于稳定。而这个趋于稳定后,内存消耗,至少是capacity的4-5倍。这样的消耗和浪费。是难以承受的。 这个方案,从实际测试来看,情况非常糟糕。所以最终还是放弃了。 ## 五、 ConcurrentLRUHashMap实现方式三:分段实现锁分离+每个段内维护一份退化链表 【实现策略】: 1、锁分离机制。内部分成了多个segement,每个segement是独立加锁,相互不干扰。 2、每个segement内部维护一个双向链表(退化链表)。每次命中/添加,就把节点移动到退化链表头部。 3、每次put操作,通过hash,散到每个segement中,判断segment的容量是否到达阈值。 如果到达阈值,则删除退化链表中最末尾的节点。 【实现】 1、重新定义HashEntry<K,V>


static class HashEntry<K, V> {

/**

* 键

*/

final K key;

/**

* hash值

*/

final int hash;

/**

* 值

*/

volatile V value;

/**

* hash链指针

*/

final HashEntry<K, V> next;

/**

* 双向链表的下一个节点

*/

HashEntry<K, V> linknext;

/**

* 双向链表的下一个节点

*/

HashEntry<K, V> linkpref;

/**

* 死亡标记

*/

AtomicBoolean dead;

}2、定义segment


3、 put操作 代码太长了,见附件吧 4、 get操作


六、 ConcurrentLRUHashMap实现方式四:

具体的做法是:

1、 对concurrentHashMap 每个节点加时间戳,每次命中只修改该节点的时间戳。

2、 集中式退化操作,每次命中并不进行退化操作。而是集中式进行退化操作(满的时候,或者时间到了)。

代码:


public CountableKey(K key,V value) {

if (value == null) {

throw new NullPointerException(“should not be null”);

}

this.value = value;

this.key = key;

refreshTimeStamp();

}

public void refreshTimeStamp(){

timestamp.set(System.currentTimeMillis());

}

final V value;

final K key;

AtomicLong timestamp = new AtomicLong();

@Override

public int compareTo(CountableKey<K, V> o) {

long thisval = this.timestamp.get();

long anotherVal = o.timestamp.get();

return (thisval < anotherVal?-1:(thisval == anotherVal?0:1));

}

}
该方案的好处:

1、 快速执行get操作。get操作的时间是“concurrentHashMap的get时间+更新时间戳”的时间。

2、 put操作,一般的put操作的时间是“concurrentHashMap的put时间”,只要还未到达容量限制。而到达容量限制以后的,需要进行“退化,清理操作”+put的时间

该方案的 可能存在的问题:

1、 命中率,该算法的命中率同linkedHashMap

2、 清除 策略:

l 满了,执行清楚。缺点:1、会出现某个时刻,写操作卡死(如果正在等待清理的话)

l 定时执行。缺点:1、性能耗费。2、读不一致仍然无法避免。\

本文来源于"阿里中间件团队播客",原文发表时间" 2011-01-18 "

时间: 2024-11-08 18:28:23

ConcurrentHaspLRUHashMap实现初探的相关文章

graphviz dot初探

graphviz dot初探 简介 现在文档都用markdown保存到github.gitlab这种代码仓库.markdown遇到最大的问题就是对图片的引用, 直接用工具绘制的图片可以引用,但是这样没法像md文件那样在git仓库中进行版本管理,而且既然文档用了描述语言, 引用图片源文件能用描述语言就更好了. dot是graphviz的一种描述语言,可以通过graphviz提供的命令行工具生成图片文件. 安装 用gentoo(prefix)安装graphviz直接emerge即可,除了默认的选项,

把《c++ primer》读薄(4-2 c和c++的数组 和 指针初探)

督促读书,总结精华,提炼笔记,抛砖引玉,有不合适的地方,欢迎留言指正. 问题1.我们知道,将一个数组赋给另一个数组,就是将一个数组的元素逐个赋值给另一数组的对应元素,相应的,将一个vector 赋给另一个vector,也是将一个vector 的元素逐个赋值给另一vector 的对应元素: //将一个vector 赋值给另一vector,使用迭代器访问vector 中的元素 vector<int> ivec(10, 20); vector<int> ivec1; for (vecto

ASP.NET ViewState 初探 (1)

ASP.NET ViewState 初探 Susan WarrenMicrosoft Corporation 2001 年 11 月 27 日 与刚接触 ASP.NET 页面的开发人员交谈时,他们通常向我提出的第一个问题就是:"那个 ViewState 到底是什么?"他们的语气中流露出的那种感觉,就象我来到一家异国情调的餐馆,侍者端上一道我从未见过的菜肴时的那种感觉 - 既疑惑不解,又充满好奇.但肯定有人认为它不错,否则就不会提供了.所以,我会先尝一尝,或许会喜欢上它,尽管它看上去的确

于EYE candy滤镜应用于补间实例初探

滤镜 在刚接触EYE candy滤镜时,我就曾经对几个常用滤镜进行实例讲解,在讲解fire(火)滤镜的时候,用到的实例就是燃烧火焰字的动画,当时是用逐帧制作来实现效果的.后来在写<FW网页设计专家门诊>的时候,也沿用了这一方法,重点是介绍fire滤镜,而不是动画的制作过程. 在看到文字颜色渐变动画的相关帖子时,忽然想起来其实eye candy也可以和FW的内置效果一样,用补间实例来实现动画效果,比逐帧改滤镜参数要容易的多. 就拿制作燃烧火焰字的例子来看: 1.输入文字,设置渐变修饰一下,按F8

ASP.NET ViewState 初探 (1) 转自msdn

asp.net ASP.NET ViewState 初探 Susan WarrenMicrosoft Corporation 2001 年 11 月 27 日 与刚接触 ASP.NET 页面的开发人员交谈时,他们通常向我提出的第一个问题就是:"那个 ViewState 到底是什么?"他们的语气中流露出的那种感觉,就象我来到一家异国情调的餐馆,侍者端上一道我从未见过的菜肴时的那种感觉 - 既疑惑不解,又充满好奇.但肯定有人认为它不错,否则就不会提供了.所以,我会先尝一尝,或许会喜欢上它,

WebService初探(推荐)〔开心本人特别看好WebService〕

web Web Service初探(推荐)<br><br><br> <br>简介<br><br>回顾过去的六年,难以想象如果没有互联网的话,网络计算会变成什么样.更早的超文本模式失败了,而互联网成功了,这其中最基本的原因可以归结为:互联网简单且无处不在.从服务提供者(如网上商店)的角度来看,只要你会打字,你就可以接受服务.从服务API的角度来看,互联网上绝大多数的活动都可以由三种方法(GET, POST, 和PUT ) 以及一种标记语

Microsoft Visual Studio.NET及Borland Delphi6初探

visual Microsoft Visual Studio.NET及Borland Delphi6初探 最近安装上了Visual Studio.NET和Borland Delphi6这两个号称下一代编程环境的东东,感觉新东西实在不少,下面就说说我的感觉. 首先说Visual Studio.NET的安装.Microsoft在这方面的霸气一直不改,我还记得当初装Visual C++5.0的时候,本来我已经有了中文版的IE3.0,可是他一定要我先装一个英文版的IE3.01,否则就不允许继续,真是不给

COM技术初探(三):一个真正的COM

一.实现ISmipleMath,IAdvancedMath接口和DllGetClassObject() 1.1 实现ISmipleMath和IAdvancedMath接口 让我们将原来的CMath 类(CMath其实就是"COM技术初探(二)COM基础知识"里的那个CMath类)修改来实现ISmipleMath接口和IAdvancedMath接口. 修改的地方如下: 1) Math.h文件 /*@**#---2003-10-29 21:33:44 (tulip)---#**@ #inc

初探C# 3.0

C#3.0已经推出好一段时间了,由于种种原因,一直没有去学习,这两天在园 子中看到老赵的拯救C# 2.0,但是我们真做的到吗?.里面提到了些C#3.0的新 特性和优势.勾起了我对3.0的兴趣,初探学习一下,分享给新手. 在 C#2.0中,微软给我们带来了一些新的特性,例如泛型,匿名委托等.然而,这 些新的特性多多少少会给人一种从别的语言中"抄"来的感觉(例如 泛型类似C++的模板,一些特性类似Java中的一些东西).但是在C#3.0中,微软 给我带来的一些新特性可能是以前所有开发语言都