jive的缓存非常简单,很适合初学者提升功力,这里将jive缓存几个关键的地方拿出来与大家分享一下。
首先,让我们对jive缓存的类库有个框架性认识,如图:
上图所列的,就是jive实现缓存所用的几个关键类。主要包括Cache,Cacheable,CacheObject,LinkedList以及和此类 似的一整套Long的Cache类。现在把以上每个功能分别叙述一下。LinkedList,是jive自己写的一个链表,在jive缓存中所有使用到队 列的地方就由它来代劳了,没有使用JDK自带的LinkedList类大概是考虑到LinkedList稍微繁琐吧;Cacheable是一个接口,其实 所有工作是通过它的实现如CacheableInt,CacheableString来完成的。主要功能是封装一些类型,并使这些类型有getSizes 的方法从而可以保存在缓存中;CacheObject是保存在缓存中的对象,每个CacheObject封装了一个Cacheable对象,并且提供了指 向两个队列
lastAccessedList和ageList节点的引用,这点在分析Cache时会知道什么作用。
OK,下面是重头戏Cache,整个缓存重要的功能就是由它搞定的。
首先,它提供了一个HashMap作为整个缓存的空间,该HashMap中每个元素是一个CacheObject。然而,这个HashMap在内存中不可 能无限大,因此必须伴随着一套管理它的方法,在缓存不够时将Cache换出或者覆盖,比如我们可以使用LRU,FIFO等等方法(可以参考操作系统和计算 机组成原理),jive很简单的采用了两个队列(精确点说还是链表)完成了这套管理,分别是
lastAccessedList和ageList 。其中LastAccessedList按照访问的频繁程度排序,最频繁的在最前,这样每次换出时就可以选取队尾的对象;而ageList则将对象按照进入的时间排序,最早的在最前。
Cache有几个主要的方法:add,remove,get,cullCache
每次有新对象需要缓存时先做两个检查:一,元素是否已经在其中;二,元素的大小是否已经超过整个缓存的90%,之后就将元素入Cache,同时 lastAccessedList和ageList头加入该元素的key,当然,ageList还要记录入Cache时间,之后调用 cullCache(),从名字就看得出是对Cache进行删减的。以下就是add方法
public synchronized void add(Object key, Cacheable object) { remove(key); int objectSize = object.getSize(); if (objectSize > maxSize * .90) { return; } size += objectSize; CacheObject cacheObject = new CacheObject(object, objectSize); cachedObjectsHash.put(key, cacheObject); LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key); cacheObject.lastAccessedListNode = lastAccessedNode; LinkedListNode ageNode = ageList.addFirst(key); ageNode.timestamp = System.currentTimeMillis(); cacheObject.ageListNode = ageNode; cullCache(); } |
再来看cullCache方法,首先判断当前的Cache大小大于所规定的最大Size,这时我们才进行删除,删除的策略是,先删除过期节点,如果完了 Size还大于最大Size的90%,接着再删去最不频繁访问的节点,你可能会问为什么要留10%出来,如果每次都不留稍多点的空间,那么下次可能很快又 要删,造成不必要的时间浪费,此时宁可牺牲可能接下来就被访问的而节约的时间,也不要把时间浪费在频繁的维护节点上。仔细推敲这个删除策略,就会发现,首 先删除时用的是ageList,第二次删除时用的是lastAccessedList。现在你知道为什么需要设两个队列了,因为可以满足不同的需要,第一 种是以时间来评定是否删除,第二种是以访问的频繁程度来评定的。如果我们还有第三种删除方式,现在你一定知道该怎么做了。
deleteExpiredEntries方法主要用来删除过期节点,从最后一个开始向前判断每个节点进入Cache的时间是否过期,如果过期了当然是删 之而后快。这里有个问题需要注意:为什么要从后向前而不是从前向后?对了,因为add时就是最老的在最后,因为入Cache都加入到对头。
private final void cullCache() { if (size >= maxSize * .97) { deleteExpiredEntries(); int desiredSize = (int)(maxSize * .90); while (size > desiredSize) { remove(lastAccessedList.getLast().object); } } } private final void deleteExpiredEntries() { if (maxLifetime <= 0) { return; } LinkedListNode node = ageList.getLast(); if (node == null) { return; } long expireTime = currentTime - maxLifetime; while(expireTime > node.timestamp) { remove(node.object); node = ageList.getLast(); if (node == null) { return; } } } |
删除操作也是被频繁调用的重点操作,主要就是将对象从Cache中删除,从两个队列中也相应的删除,当然,最重要的别忘了,将Cache当前的Size修正
public synchronized void remove(Object key) { CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key); if (cacheObject == null) { return; } cachedObjectsHash.remove(key); cacheObject.lastAccessedListNode.remove(); cacheObject.ageListNode.remove(); cacheObject.ageListNode = null; cacheObject.lastAccessedListNode = null; size -= cacheObject.size; } |
在get方法中,首先删除过期那些节点,之后对Cache的命中率或失败率进行修正,值得注意的是找到之后对lastAccessedList队列的处 理,找到之后就将元素移到对头,从此可以看出来,lastAccessedList队列的目的就是要保持最近经常访问的元素在前从而不会被删除。
public synchronized Cacheable get(Object key) { deleteExpiredEntries(); CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key); if (cacheObject == null) { cacheMisses++; return null; } cacheHits++; cacheObject.lastAccessedListNode.remove(); lastAccessedList.addFirst(cacheObject.lastAccessedListNode); return cacheObject.object; } |
对jive的缓存已经大体完了,看起来挺简单吧,其实思想的确不难。让我们再来用例子回顾一下。比如现在新建了一个Cache,来了一个字符 串"levi",它经过CacheableString和CacheObject包装后,进入Cache,此时它在lastAccessedList和 ageList的第一位,接下来又来一个字符串"pig",此时它也进Cache,排lastAccessedList和ageList第一 位,"levi"不幸成了第二,这时程序访问到"levi",于是程序修改lastAccessedList,它改变了排名成了第一,但ageList中 仍然是第二,这时又来一个"snake",假定Cache已满,就会淘汰,假定几个对象都没过期,那这时淘汰的就是"fox"了。仔细推敲,其实现非常有 意思。
另外,你一定注意到size是用字节表示的,你一定会问jive是如何计算对象大小的,这里可以去参考CacheSizes,具体做法是采用一个估算值, 比如Int型size就为4,请注意,这里是固定死在程序的,只有java这种类型大小固定的语言才能这样写,否则就要sizeof了,哈哈。