1、LRUCache的实现分析

在分析LRUCache前先对LinkedHashMap做些介绍。LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系,这种顺序有两种,一种是LRU顺序,一种是插入顺序,这可以由其构造函数public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)指定。所以,对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还做些调整Entry顺序链表的工作。
以get操作为例,如果是LRU顺序(accessOrder为true),Entry的recordAccess方法就调整get到的Entry到链表的头部去:

 public V get(Object key) {Entry<K,V> e = (Entry<K,V>)getEntry(key);if (e == null)return null;e.recordAccess(this);return e.value;}

对于put来说,LinkedHashMap重写了addEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {createEntry(hash, key, value, bucketIndex);// Remove eldest entry if instructed, else grow capacity if appropriateEntry<K,V> eldest = header.after;if (removeEldestEntry(eldest)) {removeEntryForKey(eldest.key);} else {if (size >= threshold)resize(2 * table.length);}}

addEntry中调用了boolean removeEldestEntry(Map.Entry<k,v> eldest)方法,默认实现一直返回false,也就是默认的Map是没有容量限制的。LinkedHashMap的子类可以复写该方法,当当前的size大于阈值时返回true,这样LinkedHashMap就可以从Entry顺序链表中删除最旧的Entry。这使得LinkedHashMap具有了Cache的功能,可以存储限量的元素,并具有两种可选的元素淘汰策略(LRU和FIFO),其中的LRU是最常用的。

Solr的LRUCache是基于LinkedHashMap实现的,所以LRUCache的实现真的很简单,这里列出其中核心的代码片断:

public Object init(final Map args, Object persistence, final CacheRegenerator regenerator) {//一堆解析参数参数初始化的代码//map map    map = new LinkedHashMap(initialSize, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(final Map.Entry eldest) {if (size() > limit) {// increment evictions regardless of state.// this doesn't need to be synchronized because it will// only be called in the context of a higher level synchronized block.evictions++;stats.evictions.incrementAndGet();return true;}return false;}};if (persistence==null) {// must be the first time a cache of this type is being createdpersistence = new CumulativeStats();}stats = (CumulativeStats)persistence;return persistence;}public Object put(final Object key, final Object value) {synchronized (map) {if (state == State.LIVE) {stats.inserts.incrementAndGet();}// increment local inserts regardless of state???// it does make it more consistent with the current size...inserts++;return map.put(key,value);}}public Object get(final Object key) {synchronized (map) {final Object val = map.get(key);if (state == State.LIVE) {// only increment lookups and hits if we are live.lookups++;stats.lookups.incrementAndGet();if (val!=null) {hits++;stats.hits.incrementAndGet();}}return val;}}

可以看到,LRUCache对读写操作直接加的互斥锁,多线程并发读写时会有锁的竞争问题。通常来说,Cache系统的读要远多于写,不能并发读是有些不够友好。不过,相比于Solr中其它耗时的操作来说,LRUCache的串行化读往往不会成为系统的瓶颈。LRUCache的优点是,直接套用LinkedHashMap,实现简单,缺点是,因为LinkedHashMap的get操作需要操作Entry顺序链表,所以必须对整个操作加锁。

2、FastLRUCache的实现分析

Solr1.4引入FastLRUCache作为另一种可选的实现。FastLRUCache放弃了LinkedHashMap,而是使用现在很多Java Cache实现中使用的ConcurrentHashMap。但ConcurrentHashMap只提供了高性能的并发存取支持,并没有提供对淘汰数据的支持,所以FastLRUCache主要需要做的就是这件事情。FastLRUCache的存取操作都在ConcurrentLRUCache中实现,所以我们直接过渡到ConcurrentLRUCache的实现。
ConcurrentLRUCache的存取操作代码如下:

public V get(final K key) {final CacheEntry<K,V> e = map.get(key);if (e == null) {if (islive) {stats.missCounter.incrementAndGet();}return null;}if (islive) {e.lastAccessed = stats.accessCounter.incrementAndGet();}return e.value;}public V remove(final K key) {final CacheEntry<K,V> cacheEntry = map.remove(key);if (cacheEntry != null) {stats.size.decrementAndGet();return cacheEntry.value;}return null;}public Object put(final K key, final V val) {if (val == null) {return null;}final CacheEntry e = new CacheEntry(key, val, stats.accessCounter.incrementAndGet());final CacheEntry oldCacheEntry = map.put(key, e);int currentSize;if (oldCacheEntry == null) {currentSize = stats.size.incrementAndGet();} else {currentSize = stats.size.get();}if (islive) {stats.putCounter.incrementAndGet();} else {stats.nonLivePutCounter.incrementAndGet();}// Check if we need to clear out old entries from the cache.// isCleaning variable is checked instead of markAndSweepLock.isLocked()// for performance because every put invokation will check until// the size is back to an acceptable level.// There is a race between the check and the call to markAndSweep, but// it's unimportant because markAndSweep actually aquires the lock or returns if it can't.// Thread safety note: isCleaning read is piggybacked (comes after) other volatile reads// in this method.if (currentSize > upperWaterMark && !isCleaning) {if (newThreadForCleanup) {new Thread() {@Overridepublic void run() {markAndSweep();}}.start();} else if (cleanupThread != null){cleanupThread.wakeThread();} else {markAndSweep();}}return oldCacheEntry == null ? null : oldCacheEntry.value;}

所有的操作都是直接调用map(ConcurrentHashMap)的。看下put中的代码,当map容量达到上限并且没有其他线程在清理数据(currentSize > upperWaterMark && !isCleaning),就调用markAndSweep方法清理数据,可以有3种方式做清理工作:1)在该线程同步执行,2)即时启动新线程异步执行,3)提供单独的清理线程,即时唤醒它异步执行。

markAndSweep方法那是相当的冗长,这里就不罗列出来。下面叙述下它的思路。

对于ConcurrentLRUCache中的每一个元素CacheEntry,它有个属性lastAccessed,表示最后访问的数值大小。ConcurrentLRUCache中的stats.accessCounter是全局的自增整数,当put或get Entry时,Entry的lastAccessed会被更新成新自增得到的accessCounter。 ConcurrentLRUCache淘汰数据就是淘汰那些lastAccessed较小的Entry。因为ConcurrentLRUCache没有维护以lastAccessed排序的Entry链表(否则就是LRUCache了),所以淘汰数据时就需要遍历整个Map中的元素来淘汰合适的Entry。这是不是要扯上排序呢?其实不用那么大动干戈。

这里定义几个变量,wantToKeep表示Map中需要保留的Entry个数,wantToRemove表示需要删除的个数(wantToRemove=map.size-wantToKeep),newestEntry是最大的lastAccessed值(初始是stats.accessCounter),这三个变量初始都是已知的,oldestEntry表示最小的lastAccessed,这个是未知的,可以在遍历Entry时通过比较递进到最小。Map中的Entry有3种:(a)是可以立刻判断出可以被淘汰的,也就是lastAccessed<(oldestEntry+wantToRemove)的,(b)是可以立刻判断出可以被保留的,也就是lastAccessed>(newestEntry-1000)的,(c)除上述两者之外的就是不能准确判断是否需要被淘汰的。对于遍历一趟Map中的Entry来说,极好的情况是如果淘汰掉满足(a)的Entry后Map大小降到了wantToKeep,这种情况的典型代表是对Cache只有get和put操作,使得lastAccessed在Map中能保持连续;极坏的情况是,可能满足(a)的Entry不够多甚至没有。但遍历一趟Map至少有一个效果是,会把需要处理的Entry范围缩小到满足(c)的。如此反复迭代,一定使得Map容量调到wantToKeep。而对这个淘汰,也要考虑一个现实情况是,wantToKeep往往是接近于map.size(比如等于0.9*map.size)的,如果remove操作不是很多,那么并不需要很多次遍历就可以完成清理工作。

ConcurrentLRUCache淘汰数据的基本思想如上所述。它的执行过程可以分为3个阶段。第一个阶段就是遍历Map中的每个Entry,如果满足(a)就remove,满足(b)则跳过,满足(c)则放到新map中。一遍下来后,如果map.size还大于wantToKeep,第二个阶段就再重复上述过程(实现上,Solr用了个变量numPasses,似乎想做个开关控制遍历几次,当前就固定成一次)。完了如果map.size还大于wantToKeep,第三阶段再遍历一遍Map,但这次使用PriorityQueue来提取出还需要再淘汰的N个最old的Entry,这样一次下来就收工了。需要补充一点,上面提到的wantToKeep在代码中是acceptableWaterMark和lowerWaterMark,也就是如果遍历后达到acceptableWaterMark就算完成,但操作是按lowerWaterMark的要求来。

这个算法的时间复杂度是2n+kln(k)(k值在实际大多数情况下会很小),相比于直接的堆排,通常会更快些。

3、总结

LRUCache和FastLRUCache两种Cache实现是两种很不同的思路。两者的相同点是,都使用了现成的Map来维护数据。不同点是如何来淘汰数据。LRUCache(也就是LinkedHashMap)格外维护了一个结构,在做存取操作时同时更新该结构,优点在于淘汰操作是O(1)的,缺点是需要对存取操作加互斥锁。FastLRUCache正相反,它没有额外维护新的结构,可以由ConcurrentHashMap支持并发读,但put操作中如果需要淘汰数据,淘汰过程是O(n)的,因为整个过程不加锁,这也只会影响该次put的性能,而FastLRUCache也可选成起独立线程异步执行来降低影响。而另一个Cache实现Ehcache,它在淘汰数据就是同步的,不过它限定了每次淘汰数据的大小(通常都少于5个),所以同步情况下性能不会太受影响。

原文:http://www.cnblogs.com/chenying99/archive/2012/08/02/2620703.html

转载于:https://www.cnblogs.com/veins/p/3892165.html

LRUCache和FastLRUCache实现分析相关推荐

  1. Android开发学习之路-LruCache使用和源码分析

    LruCache的Lru指的是LeastRecentlyUsed,也就是近期最少使用算法.也就是说,当我们进行缓存的时候,如果缓存满了,会先淘汰使用的最少的缓存对象. 为什么要用LruCache?其实 ...

  2. LruCache之LruCache分析

    转LruCache之LruCache分析 LruCache 分析 LruCache 是 Android 的一个内部类,提供了基于内存实现的缓存 用法 //获取系统分配给每个应用程序的最大内存,每个应用 ...

  3. Solr Cache使用介绍及分析

    本文将介绍Solr查询中涉及到的Cache使用及相关的实现.Solr查询的核心类就是SolrIndexSearcher,每个core通常在同一时刻只由当前的SolrIndexSearcher供上层的h ...

  4. 【转载】solr教程,值得刚接触搜索开发人员一看

    转载:http://blog.csdn.net/awj3584/article/details/16963525 Solr调研总结 开发类型 全文检索相关开发 Solr版本 4.2 文件内容 本文介绍 ...

  5. Configuring solrconfig.xml (1)

    1 Configuring solrconfig.xml solrconfig.xml文件中包含了Solr需要的大部分参数,当你配置Solr时,你会经常性地使用solrconfig.xml.你可能会配 ...

  6. 浅谈solrCloud的分布式设计

    在solr cloud 中一个collection是一个 文档的 集合.一个collection可以分为多个slice,     每个slice的实例和其备份(replica)都称为shard.一个s ...

  7. solr教程,值得刚接触搜索开发人员一看(转载:http://blog.csdn.net/awj3584/article/details/16963525)

    Solr调研总结 开发类型 全文检索相关开发 Solr版本 4.2 文件内容 本文介绍solr的功能使用及相关注意事项;主要包括以下内容:环境搭建及调试;两个核心配置文件介绍;维护索引;查询索引,和在 ...

  8. Solr调研总结(很详细很全面)

    Solr调研总结 开发类型 全文检索相关开发 Solr版本 4.2 文件内容 本文介绍solr的功能使用及相关注意事项;主要包括以下内容:环境搭建及调试;两个核心配置文件介绍;维护索引;查询索引,和在 ...

  9. solr教程,值得刚接触搜索开发人员一看

    http://blog.csdn.net/awj3584/article/details/16963525 Solr调研总结 开发类型 全文检索相关开发 Solr版本 4.2 文件内容 本文介绍sol ...

  10. solr 教程,值得刚接触搜索开发人员一看

    2019独角兽企业重金招聘Python工程师标准>>> 1. Solr 是什么? Solr它是一种开放源码的.基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序 ...

最新文章

  1. Java5线程并发库之保障变量的原子性操作
  2. 软件破解工具整理收集
  3. linux svn 开机启动
  4. 1-6 数据查询(下)——复杂查询
  5. 1026 Table Tennis (30 分) 未完成【难度: 难 / 知识点: 模拟】
  6. 力扣: 88. 合并两个有序数组
  7. 阿里云联合中国信通院发布《云计算开放应用架构》标准,加速云原生应用规模化落地进程
  8. 专科 java转go 翱翔之路(一)基础语法:变量声明,匿名函数,结构体,函数,map
  9. [Leedcode][JAVA][第105题][从前序与中序遍历序列构造二叉树][栈][递归][二叉树]
  10. 新生的 XInclude
  11. linux用户怎么归纳到组,Linux用户和组命令总结
  12. Echarts实现双y轴(不同刻度)
  13. 08.音频系统:第004课_Android音频系统详解:第001节_分析思路
  14. 南京数海文化传媒有限公司官网上线 | LTD文化传媒行业案例分享
  15. 小程序视频不显示进度条,且不能滑屏快进
  16. 旧电脑搭建linux服务器
  17. Recsys'21 | 基于Transformers的行为序列建模
  18. 移动物联网应用典型案例福建8项目入选,四信携手产业共赢
  19. MyBatis动态sql之choose(when、otherwise)用法
  20. day02_《谷粒商城》的完整流程(详细版一)

热门文章

  1. Winsw将jar包部署为windows服务
  2. 针对EasyUI的checkbox进行扩展
  3. C++17 部分实用特性
  4. 25. 熟悉非标准的哈希容器
  5. 阿里代码规范pdf_看完阿里的代码规范,立马学会代码分层,再也不会被同事怼...
  6. 下载安装VS Code以及简单的配置使用
  7. 八皇后问题-python描述
  8. 计算机机房使用多大的光纤引入,弱电施工如何确定光纤使用的芯数
  9. word批量打印助手_如何批量打印数十份甚至上百份Word文档
  10. 学生信息管理系统(附运行效果图和源码下载)分页技术(后台封装json数据传递到前端显示,动态分页等)(Mybatis,json,ajax,jQuery实用整合示例)