guava cache

简单说一下,guava的代码质量极高,写法很值得借鉴;其次cache是计算机科学中非常常用且有效的技术,从处理器缓存到到应用程序本地缓存到分布式缓存,缓存的存在意义是存储器的金字塔模型,使用内存可以肯定比硬盘更快。

可以先看下guava提供的基本关于缓存的功能

  1. 清除策略,根据引用类型(由gc决定)、size、根据时间(access time, write time)、手动删除
  2. 删除通知, removal listener,允许用户自定义一些行为,可以异步或者同步(RemovalListeners.asynchronous封装)
  3. refresh策略,相对write time, access time来刷新,通过reload方法来实现,可以同步也可以异步(CacheLoader.asyncReloading封装)
  4. 统计功能
  5. 视图功能

数据结构

搞清楚guava-cache核心在于数据结构,它层层调用下底层的数据结构实现是LocalCache

看类图的继承关系就可以知道它实现了ConcurrentMap的语义,因此常见的Map操作都是线程安全的,但是我们的重点是看如何实现清楚策略的,实际上删除通知都很容易想到,统计功能也应该只要的必要的地方插入一个扩展就好,视图的功能可以预见仅仅是这个Map提供的一个功能,那么问题仅仅在于如何处理并发,这可以参考ConcurrentHashMap来看

先看LocalCache保存了哪些状态对象,这些对象在操作过程提供了哪些帮助



有几个值是值得关注的,Segment[],参考了分段锁的实现,把Map分成几个段,每个段持有一把锁,使用更细粒度的实现更好的并发;concurrencyLevel决定了几个分段锁,默认是4;removalListener就是一个删除时候的监听器,存放删除Key,Value的时候回调,可以看出来只能有一个;removalNotificationQueue是一个很有意思的队列,在元素被删除的时候会把对应的事件扔进去,给removalListener调用,是一个暂时存放事件的容器。

LocalCache的很多功能委托给了Segment实现,因此来看看Segment的细节。

在Segment类上有一段总体的介绍告诉我们,segment维护一个哈希表,并且总是处在一个一致的状态,因此不加锁可以直接读;每个node(也就是ReferenceEntry)的next字段都是不可变的(final)。所有的list操作仅在表头操作,因此一旦有元素被删除,必须要重新创建entry,这种情况因为hash table的每个bin上链表长度都不会太大,因此可以很好运行。

在segment有几个数据结构,首先维护一个table,使用的是java.util.concurrent.acomic.AtomicReferenceArray,内部操作时候使用CAS机制达到非阻塞;keyReferenceQueue,valueReferenceQueue都是ReferenceQueue,熟悉java引用类型的都知道,这是用来让weak引用和soft引用被回收的时候能收到通知的一个队列,目的也很清晰,当gc回收之后我们能得到通知把相应的entry清除掉。最有意思的应该还是recencyQueue,writeQueue,accessQueue。当然有个小细节需要额外注意,count字段记录了segment活着的数量,它的声明上有个volatile,这个声明很重要,能够保证内存的可见性,所有写操作在完成的时候一定要写一个volatile,这样才能保证下一个读请求能读到最新数据。

先看WriteQueue, guava为其单独实现了一个Queue, WriteQueue继承AbstractQueue<ReferenceEntry<K, V>>, 设置了一个队列头,添加元素和取元素,仅仅是把Reference的链表关系调整下,操作都是O(1)量级。WriteQueue操作与ReferenceEntry有着密切的关系,ReferenceEntry接口中维护一个双向链表结构标识写的顺序,当entry的nextInWriteQueue是NullEntry的时候就代表已经不在队列里了(这里使用零值的概念来杜绝null的存在,下面也会阐释,是一个很好的借鉴方法)。WriteQueue的head既是头也是尾,可以看做一个环形队列,好处是判断Reference是否存在队列里主要看下一个元素是不是零值就可以。

accessQueue与writeQueue的实现原理也一致,仅仅是在ReferenceEntry的字段变了。

可以看出,accessQueue与writeQueue本身是非线程安全的,在对应的Field上也能看到 @GuardedBy("this")的注解,也能知道,操作这些队列的时候,必须要加锁,并且锁对象本身是this(Segment自己继承了ReentrantLock)。

那么假如说每一个读操作,缓存命中的情况,也需要拿锁来修改accessQueue,肯定是很不好,因此引入了一个recencyQueue,本身就是一个ConcurrentLinkedQueue,每次读只需要更改ConcurrentLinkedQueue(非阻塞)就好。那么什么时候把recencyQueue的数据弄到accessQueue里呢,就是每次持有锁的时候去顺便解决下这个问题,把开销分摊到每次拿到锁的操作上。这种策略在redis中也大量使用。

关键路径分析

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {checkNotNull(key);checkNotNull(loader);try {if (count != 0) { // read-volatile// don't call getLiveEntry, which would ignore loading valuesReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {long now = map.ticker.read();V value = getLiveValue(e, now);if (value != null) {recordRead(e, now);statsCounter.recordHits(1);return scheduleRefresh(e, key, hash, value, now, loader);}ValueReference<K, V> valueReference = e.getValueReference();if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// at this point e is either null or expired;return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {Throwable cause = ee.getCause();if (cause instanceof Error) {throw new ExecutionError((Error) cause);} else if (cause instanceof RuntimeException) {throw new UncheckedExecutionException(cause);}throw ee;} finally {postReadCleanup();}
}

上来先判断count的数量,由于count是volatile的,因此这步可以保证在这以后看到的内存是最新的(这么说法不准确的,但是确实好理解)。很明显if里面会去判断有没有这个元素,因此根据对应的key和hash找到对应的桶(bucket或者bin?),拿到ReferenceEntry(getEntry),还要证明下没有过期且不是正在加载(getLiveValue),没过期了还要看看需不需要定期刷新(scheduleRefresh),发现都没问题了就可以返回了。但是如果有一丢丢问题,就排除情况,发现是正在加载(valueReference.isLoading()),那么就等待加载成功就好(实际上会调用valueReference.waitForValue方法)

关键来看lockedGetOrLoad。上来先lock()加锁,先不管CleanUp这些操作,上来直接看根据hash获取Reference,比较key,判断是否在loading、是否已经被垃圾回收、是否已经过期,如果都没有又找到了,直接返回value就好了,这里的过期时间判断是此处新取的时间。为什么外面已经判断了entry不存在,这里又会有存在的情况呢?答案是上面的代码和该出lock的过程有一段时间差,要考虑这段时间差其他线程把改key加载的场景,这也是多线程编程复杂的原因,需要区分很清楚哪里是临界区,在临界区外部的状态,在进入到临界区内后,完全可能改变,需要重新判断。在并发编程中,只有临界区里的状态是可信的,可以不用担心并发,但是临界区外的状态只能说作为一层挡板,为了性能考虑,不要去掺和锁的竞争。

继续看,判断下要不要新创建Entry,如果需要就创建一个LoadingValueReference,其实就是一个占位元素,然后把ValueRefence正确set进去就好,这里可能会复用过期的ValueReference,假如是新创建的只能加到头部,然后解锁,cleanUp,cleanUp等会统一看。
到下面可以看到,这时候同步加载,加锁是用entry的来加锁,使用内置锁synchronized来做,有就是说到加载的时候不可中断的。

loadSync方法是同步加载,真正会执行写入操作,并且来改变segment的结构,可以看到加载操作使用的锁和segment的锁是分离的,通过细粒度的锁使用提高并发。loadSync本身可能平淡无奇,往下看顶多是在Future的用法上不熟悉上可能有点绕,真正关键部分:

boolean storeLoadedValue(K key, int hash, LoadingValueReference<K, V> oldValueReference, V newValue) {lock();try {long now = map.ticker.read();preWriteCleanup(now);int newCount = this.count + 1;if (newCount > this.threshold) { // ensure capacityexpand();newCount = this.count + 1;}AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {K entryKey = e.getKey();if (e.getHash() == hash&& entryKey != null&& map.keyEquivalence.equivalent(key, entryKey)) {ValueReference<K, V> valueReference = e.getValueReference();V entryValue = valueReference.get();// replace the old LoadingValueReference if it's live, otherwise// perform a putIfAbsentif (oldValueReference == valueReference|| (entryValue == null && valueReference != UNSET)) {++modCount;if (oldValueReference.isActive()) {RemovalCause cause =(entryValue == null) ? RemovalCause.COLLECTED : RemovalCause.REPLACED;enqueueNotification(key, hash, entryValue, oldValueReference.getWeight(), cause);newCount--;}setValue(e, key, newValue, now);this.count = newCount; // write-volatileevictEntries(e);return true;}// the loaded value was already clobberedenqueueNotification(key, hash, newValue, 0, RemovalCause.REPLACED);return false;}}++modCount;ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);setValue(newEntry, key, newValue, now);table.set(index, newEntry);this.count = newCount; // write-volatileevictEntries(newEntry);return true;} finally {unlock();postWriteCleanup();}
}

首先这个需要segment的锁,其实这样看可以看做是一个把之前断掉的加载过程连起来,跟hashMap一样,可能需要扩张表的大小(expand),然后就是一个老流程,这里最后需要检查下原来的元素有没被垃圾回收,有没过期,然后设置新值就好。流程很简单,但是工作量不少

常见类的调用关系


一些代码借鉴

  1. 如何避免null

  2. 如何使用Factory模式来实现多态

  3. 合适的数据结构已经成功了一半

转载于:https://www.cnblogs.com/snailLx/p/9352078.html

guava cache简单学习笔记相关推荐

  1. spring boot guava cache 缓存学习

    http://blog.csdn.net/hy245120020/article/details/78065676 ****************************************** ...

  2. 特征提取算法简单学习笔记

    update 2021.04.22 这几年的经验下来,以前以为特征提取的方法时共通的,注意力都在后续算法部分,现在的感受是,不同领域算法反而很多时候时共通的,特征提取差异很大,不能简单的一言以蔽之,这 ...

  3. [mmu/cache]-ARM cache的学习笔记-一篇就够了

    ★★★ 个人博客导读首页-点击此处 ★★★ . 说明: 在默认情况下,本文讲述的都是ARMV8-aarch64架构,linux kernel 64位 . 相关文章 1.ARM MMU的学习笔记-一篇就 ...

  4. Guava Cache 使用学习

    Guava -Caache Guava缓存值CacheBuilder介绍-参考 Google -CachesExplained wiki 缓存框架Guava Cache部分源码分析 概述 缓存是日常开 ...

  5. 网易蜂巢简单学习笔记

    这个笔记是在学习网易云课堂Java Web微专业的入门课程中单个章节时记下的. 主要是一些命令以及网易蜂巢的简单使用. 简单笔记

  6. Scaled-YOLOv4 简单学习笔记

    参考链接: 全文翻译[Scaled-YOLOv4: Scaling Cross Stage Partial Netw]_聪明的小菠菜-CSDN博客 论文阅读笔记 之 YOLOv4 & scal ...

  7. python读取字典元素笔记_Python中列表、字典、元组数据结构的简单学习笔记

    列表 列表是Python中最具灵活性的有序集合对象类型.与字符串不同的是,列表可以包含任何类型的对象:数字.字符串甚至其他列表.列表是可变对象,它支持原地修改的操作. Python的列表是: 任意对象 ...

  8. Linux——软件包简单学习笔记

    Linux中的是那种软件包:  (这里学习是基于redHat的Cent-OS) 1: 二进制软件包管理(RPM.YUM) 2:源代码包安装 3: 脚本安装(Shell或Java脚本) 一: 二进制软件 ...

  9. 伯努利数简单学习笔记

    伯努利数 [百度百科:里面有很多应用] 定义与公式 我们常用BiB_iBi​定义第iii个伯努利数. 生成函数定义方式: zez−1=∑n=0∞Bnznn!\frac{z}{e^z-1}=\sum\l ...

最新文章

  1. 插槽 查看硬盘状态_摄影路上的“全能”伴侣 | LaCie DJI Copilot 移动硬盘
  2. opencv cv2.LUT()(使用查找表中的值填充输出数组)
  3. XML引入多scheme文件约束简单示例
  4. “自启动”树莓派上的 .NET Core 3.0 环境
  5. matplotlib散点图笔记
  6. 小程序 php转excel,做微信小程序上传数据 数据格式?-微信 上传数据 生成excle
  7. Python项目实践:自动轨迹绘制(根据脚本绘制图形)
  8. 配置使用4台主机实现12台主机的集群
  9. 父组件和子组件同是使用 beforeDestroy 钩子 保存同一份数据
  10. iOS学习笔记32 - 锚点
  11. AutoCAD彻底卸载和清理注册表
  12. Mybatis常用的OGNL表达式
  13. 基于SpringCloud的enum枚举值国际化处理实践
  14. 阿里1688产品图片和视频资料下载
  15. day17--途牛旅游项目-激活功能
  16. 天河二号超级计算机能买到吗,天河二号计算机是巨型机吗
  17. cmos逻辑门传输延迟时间_如何判断输出的高低电平(三态门)?
  18. uniapp样式选择器最全详解
  19. 湘潭哪里学计算机编程,湘潭哪里学机器人编程?湘潭学机器人编程的学校有哪些?...
  20. Acoustics | 声音时间检测:日常声音理解

热门文章

  1. 《Arduino实战》——第1章 你好Arduino
  2. 表单验证Jquery扩展方法类
  3. 关于程序猿的几个阶段!
  4. Quartz使用示例总结
  5. JS小数位保留两位小数
  6. WCF+Silverlight一个简单的RSS阅读器(二)
  7. centos 7 安装nfs 服务
  8. python:文件操作
  9. 华为nova 2 Plus魔镜版818欢购热潮凶猛来袭
  10. linux 程序自启