guava作为google开源出来的工具库,google自己对guava的描述:the guava project contains several of google's core libraries that we rely on in our java-based projects: collections, caching, primitives support, concurrency libraries, common annotations, string processing, i/o, and so forth.作为google的core libraries,直接提供cache实现,足以证明cache应用的广泛程度。
然而作为工具库中的一部分,我们自然不能期待guava对cache有比较完善的实现。因而guava中的cache只能用于一些把cache作为一种辅助设计的项目或者在项目的前期为了实现简单而引入。
在guava cachebuilder的注释中给定guava cache以下的需求:
- automatic loading of entries into the cache
- least-recently-used eviction when a maximum size is exceeded
- time-based expiration of entries, measured since last access or last write
- keys automatically wrapped in weakreference
- values automatically wrapped in weakreference or softreference soft
- notification of evicted (or otherwise removed) entries
- accumulation of cache access statistics
对于这样的需求,如果要我们自己来实现,我们应该怎么设计?对于我来说,对于其核心实现我会做如下的设计:
- 定义一个cacheconfig类用于纪录所有的配置,如cacheloader,maximum size、expire time、key reference level、value reference level、eviction listener等。
- 定义一个cache接口,该接口类似map(或concurrentmap),但是为了和map区别开来,因而重新定义一个cache接口。
- 定义一个实现cache接口的类cacheimpl,它接收cacheconfig作为参数的构造函数,并将cacheconfig实例保存在字段中。
- 在实现上模仿concurrenthashmap的实现方式,有一个segment数组,其长度由配置的concurrencylevel值决定。为了实现最近最少使用算法(lru),添加accessqueue和writequeue字段,这两个queue内部采用双链表,每次新创建一个entry,就将这个entry加入到这两个queue的末尾,而每读取一个entry就将其添加到accessqueue的末尾,没更新一个entry将该entry添加到writequeue末尾。为了实现key和value上的weakreference、softreference,添加referencequeue类型的keyreferencequeue和valuereferencequeue字段。
- 在每次调用方法之前都遍历accessqueue和writequeue,如果发现有entry已经expire,就将该entry从这两个queue上和cache中移除。然后遍历keyreferencequeue和valuereference,如果发现有项存在,同样将它们移除。在移除时如果有evictionlistener注册着,则调用该listener。
- 对segment实现,它时一个cacheentry数组,cacheentry是一个链节点,它包含hash、key、vlaue、next。cacheentry根据是否需要包装在weakreference中创建weakentry或strongentry,而对value根据是否需要包装在weakreference、softreference中创建weakvaluereference、softvaluereference、strongvaluereference。在get操作中对于需要使用cacheloader加载的值先添加一个具有loadingvaluereference值的entry,这样可以保证同一个key只加载依次。在加载成功后将loadingvaluereference根据配置替换成其他weak、soft、strong valuereference。
- 对于cache access statistics,只需要有一个类在需要的地方做一些统计计数即可。
- 最后我必须得承认以上的设计有很多是对guava cache的参考,我有点后悔没有在看源码之前考虑这个问题,等看过以后思路就被它的实现给羁绊了。。。。
guava cache的数据结构
因为新进一家公司,要熟悉新公司项目以及项目用到的第三方库的代码,因而几个月来看了许多代码。然后越来越发现要理解一个项目的最快方法是先搞清楚该项目的底层数据结构,然后再去看构建于这些数据结构以上的逻辑就会容易许多。记得在还是学生的时候,有在一本书上看到过一个大牛说的一句话:程序=数据结构+算法;当时对这句话并不是和理解,现在是很赞同这句话,我对算法接触的不多,因而我更倾向于将这里的算法理解长控制数据流动的逻辑。因而我们先来熟悉一下guava cache的数据结构。
cache类似于map,它是存储键值对的集合,然而它和map不同的是它还需要处理evict、expire、dynamic load等逻辑,需要一些额外信息来实现这些操作。在面向对象思想中,经常使用类对一些关联性比较强的数据做封装,同时把操作这些数据相关的操作放到该类中。因而guava cache使用referenceentry接口来封装一个键值对,而用valuereference来封装value值。这里之所以用reference命令,是因为guava cache要支持weakreference key和softreference、weakreference value。
valuereference
对于valuereference,因为guava cache支持强引用的value、softreference value以及weakreference value,因而它对应三个实现类:strongvaluereference、softvaluereference、weakvaluereference。为了支持动态加载机制,它还有一个loadingvaluereference,在需要动态加载一个key的值时,先把该值封装在loadingvaluereference中,以表达该key对应的值已经在加载了,如果其他线程也要查询该key对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次(可以在evict以后重新加载)。在该只加载完成后,将loadingvaluereference替换成其他valuereference类型。对新创建的loadingvaluereference,由于其内部oldvalue的初始值是unset,它isactive为false,isloading为false,因而此时的loadingvaluereference的isactive为false,但是isloading为true。每个valuereference都纪录了weight值,所谓weight从字面上理解是“该值的重量”,它由weighter接口计算而得。weight在guava cache中由两个用途:1. 对weight值为0时,在计算因为size limit而evict是忽略该entry(它可以通过其他机制evict);2. 如果设置了maximumweight值,则当cache中weight和超过了该值时,就会引起evict操作。但是目前还不知道这个设计的用途。最后,guava cache还定义了stength枚举类型作为valuereference的factory类,它有三个枚举值:strong、soft、weak,这三个枚举值分别创建各自的valuereference,并且根据传入的weight值是否为1而决定是否要创建weight版本的valuereference。以下是valuereference的类图:
这里valuereference之所以要有对referenceentry的引用是因为在value因为weakreference、softreference被回收时,需要使用其key将对应的项从segment的table中移除;copyfor()函数的存在是因为在expand(rehash)重新创建节点时,对weakreference、softreference需要重新创建实例(个人感觉是为了保持对象状态不会相互影响,但是不确定是否还有其他原因),而对强引用来说,直接使用原来的值即可,这里很好的展示了对彼变化的封装思想;notifiynewvalue只用于loadingvaluereference,它的存在是为了对loadingvaluereference来说能更加及时的得到cacheloader加载的值。
referenceentry
referenceentry是guava cache中对一个键值对节点的抽象。和concurrenthashmap一样,guava cache由多个segment组成,而每个segment包含一个referenceentry数组,每个referenceentry数组项都是一条referenceentry链。并且一个referenceentry包含key、hash、valuereference、next字段。除了在referenceentry数组项中组成的链,在一个segment中,所有referenceentry还组成access链(accessqueue)和write链(writequeue),这两条都是双向链表,分别通过previousaccess、nextaccess和previouswrite、nextwrite字段链接而成。在对每个节点的更新操作都会将该节点重新链到write链和access链末尾,并且更新其writetime和accesstime字段,而没找到一个节点,都会将该节点重新链到access链末尾,并更新其accesstime字段。这两个双向链表的存在都是为了实现采用最近最少使用算法(lru)的evict操作(expire、size limit引起的evict)。
guava cache中的referenceentry可以是强引用类型的key,也可以weakreference类型的key,为了减少内存使用量,还可以根据是否配置了expireafterwrite、expireafteraccess、maximumsize来决定是否需要write链和access链确定要创建的具体reference:strongentry、strongwriteentry、strongaccessentry、strongwriteaccessentry等。创建不同类型的referenceentry由其枚举工厂类entryfactory来实现,它根据key的strongth类型、是否使用accessqueue、是否使用writequeue来决定不同的entryfactry实例,并通过它创建相应的referenceentry实例。referenceentry类图如下:
writequeue和accessqueue
为了实现最近最少使用算法,guava cache在segment中添加了两条链:write链(writequeue)和access链(accessqueue),这两条链都是一个双向链表,通过referenceentry中的previousinwritequeue、nextinwritequeue和previousinaccessqueue、nextinaccessqueue链接而成,但是以queue的形式表达。writequeue和accessqueue都是自定义了offer、add(直接调用offer)、remove、poll等操作的逻辑,对于offer(add)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对remove操作,直接从该链中移除该节点;对poll操作,将头节点的下一个节点移除,并返回。
static final class writequeue extends abstractqueue> {
final referenceentry head = new abstractreferenceentry() ....
@override
public boolean offer(referenceentry entry) {
// unlink
connectwriteorder(entry.getpreviousinwritequeue(), entry.getnextinwritequeue());
// add to tail
connectwriteorder(head.getpreviousinwritequeue(), entry);
connectwriteorder(entry, head);
return true;
}
@override
public referenceentry peek() {
referenceentry next = head.getnextinwritequeue();
return (next == head) ? null : next;
}
@override
public referenceentry poll() {
referenceentry next = head.getnextinwritequeue();
if (next == head) {
return null;
}
remove(next);
return next;
}
@override
public boolean remove(object o) {
referenceentry e = (referenceentry) o;
referenceentry previous = e.getpreviousinwritequeue();
referenceentry next = e.getnextinwritequeue();
connectwriteorder(previous, next);
nullifywriteorder(e);
return next != nullentry.instance;
}
@override
public boolean contains(object o) {
referenceentry e = (referenceentry) o;
return e.getnextinwritequeue() != nullentry.instance;
}
....
}
对于不需要维护writequeue和accessqueue的配置(即没有expire time或size limit的evict策略)来说,我们可以使用discarding_queue以节省内存:
static final queueextends object> discarding_queue = new abstractqueue