Guava Cache的数据结?/strong>
因ؓ新进一家公司,要熟悉新公司目以及目用到的第三方库的代码Q因而几个月来看了许多代码。然后越来越发现要理解一个项目的最快方法是先搞清楚该项目的底层数据l构Q然后再ȝ构徏于这些数据结构以上的逻辑׃Ҏ许多。记得在q是学生的时候,有在一本书上看到过一个大牛说的一句话Q程序=数据l构Q算法;当时对这句话q不是和理解Q现在是很赞同这句话Q我对算法接触的不多Q因而我更們于将q里的算法理解长控制数据动的逻辑。因而我们先来熟悉一下Guava Cache的数据结构?br />
CachecM于MapQ它是存储键值对的集合,然而它和Map不同的是它还需要处理evict、expire、dynamic load{逻辑Q需要一些额外信息来实现q些操作。在面向对象思想中,l常使用cd一些关联性比较强的数据做装Q同时把操作q些数据相关的操作放到该cM。因而Guava Cache使用ReferenceEntry接口来封装一个键值对Q而用ValueReference来封装Value倹{这里之所以用Reference命oQ是因ؓGuava Cache要支持WeakReference Key和SoftReference、WeakReference value?br />
ValueReference
对于ValueReferenceQ因为Guava Cache支持强引用的Value、SoftReference Value以及WeakReference ValueQ因而它对应三个实现c:StrongValueReference、SoftValueReference、WeakValueReference。ؓ了支持动态加载机Ӟ它还有一个LoadingValueReferenceQ在需要动态加载一个key的值时Q先把该值封装在LoadingValueReference中,以表达该key对应的值已l在加蝲了,如果其他U程也要查询该key对应的|p得到该引用,q且{待改值加载完成,从而保证该值只被加载一ơ(可以在evict以后重新加蝲Q。在该只加蝲完成后,LoadingValueReference替换成其他ValueReferencecd。对新创建的LoadingValueReferenceQ由于其内部oldValue的初始值是UNSETQ它isActive为falseQisLoading为falseQ因而此时的LoadingValueReference的isActive为falseQ但是isLoading为true。每个ValueReference都纪录了weight|所谓weight从字面上理解?#8220;该值的重量”Q它由Weighter接口计算而得。weight在Guava Cache中由两个用途:1. 对weightgؓ0Ӟ在计因为size limit而evict是忽略该EntryQ它可以通过其他机制evictQ;2. 如果讄了maximumWeight|则当Cache中weight和超q了该值时Q就会引起evict操作。但是目前还不知道这个设计的用途。最后,Guava Cacheq定义了Stength枚Dcd作ؓValueReference的factoryc,它有三个枚D|Strong、Soft、WeakQ这三个枚D值分别创建各自的ValueReferenceQƈ且根据传入的weight值是否ؓ1而决定是否要创徏Weight版本的ValueReference。以下是ValueReference的类图:
q里ValueReference之所以要有对ReferenceEntry的引用是因ؓ在Value因ؓWeakReference、SoftReference被回收时Q需要用其key对应的从Segment的table中移除;copyFor()函数的存在是因ؓ在expand(rehash)重新创徏节点Ӟ对WeakReference、SoftReference需要重新创建实例(个h感觉是ؓ了保持对象状态不会相互媄响,但是不确定是否还有其他原因)Q而对强引用来_直接使用原来的值即可,q里很好的展CZ对彼变化的封装思想QnotifiyNewValue只用于LoadingValueReferenceQ它的存在是Z对LoadingValueReference来说能更加及时的得到CacheLoader加蝲的倹{?br />
ReferenceEntry
ReferenceEntry是Guava Cache中对一个键值对节点的抽象。和ConcurrentHashMap一PGuava Cache由多个Segmentl成Q而每个Segment包含一个ReferenceEntry数组Q每个ReferenceEntry数组w是一条ReferenceEntry链。ƈ且一个ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry数组中l成的链Q在一个Segment中,所有ReferenceEntryq组成access链(accessQueueQ和write链(writeQueueQ,q两条都是双向链表,分别通过previousAccess、nextAccess和previousWrite、nextWrite字段链接而成。在Ҏ个节点的更新操作都会该节点重新铑ֈwrite铑֒access链末,q且更新其writeTime和accessTime字段Q而没扑ֈ一个节点,都会该节点重新铑ֈaccess链末,q更新其accessTime字段。这两个双向链表的存在都是ؓ了实现采用最q最用算法(LRUQ的evict操作Qexpire、size limit引v的evictQ?br />
Guava Cache中的ReferenceEntry可以是强引用cd的keyQ也可以WeakReferencecd的keyQؓ了减内存用量Q还可以Ҏ是否配置了expireAfterWrite、expireAfterAccess、maximumSize来决定是否需要write铑֒access铄定要创徏的具体ReferenceQStrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry{。创Z同类型的ReferenceEntry由其枚D工厂cEntryFactory来实玎ͼ它根据key的Strongthcd、是否用accessQueue、是否用writeQueue来决定不同的EntryFactry实例Qƈ通过它创建相应的ReferenceEntry实例。ReferenceEntrycd如下Q?
WriteQueue和AccessQueue
Z实现最q最用算法,Guava Cache在Segment中添加了两条链:write链(writeQueueQ和access链(accessQueueQ,q两条链都是一个双向链表,通过ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue链接而成Q但是以Queue的Ş式表达。WriteQueue和AccessQueue都是自定义了offer、addQ直接调用offerQ、remove、poll{操作的逻辑Q对于offerQaddQ操作,如果是新加的节点Q则直接加入到该铄l尾Q如果是已存在的节点Q则该节点链接的链;对remove操作Q直接从该链中移除该节点Q对poll操作Q将头节点的下一个节点移除,q返回?br />
static final class WriteQueue<K, V> extends AbstractQueue<ReferenceEntry<K, V>> {
final ReferenceEntry<K, V> head = new AbstractReferenceEntry<K, V>() ....
@Override
public boolean offer(ReferenceEntry<K, V> entry) {
// unlink
connectWriteOrder(entry.getPreviousInWriteQueue(), entry.getNextInWriteQueue());
// add to tail
connectWriteOrder(head.getPreviousInWriteQueue(), entry);
connectWriteOrder(entry, head);
return true;
}
@Override
public ReferenceEntry<K, V> peek() {
ReferenceEntry<K, V> next = head.getNextInWriteQueue();
return (next == head) ? null : next;
}
@Override
public ReferenceEntry<K, V> poll() {
ReferenceEntry<K, V> next = head.getNextInWriteQueue();
if (next == head) {
return null;
}
remove(next);
return next;
}
@Override
public boolean remove(Object o) {
ReferenceEntry<K, V> e = (ReferenceEntry) o;
ReferenceEntry<K, V> previous = e.getPreviousInWriteQueue();
ReferenceEntry<K, V> next = e.getNextInWriteQueue();
connectWriteOrder(previous, next);
nullifyWriteOrder(e);
return next != NullEntry.INSTANCE;
}
@Override
public boolean contains(Object o) {
ReferenceEntry<K, V> e = (ReferenceEntry) o;
return e.getNextInWriteQueue() != NullEntry.INSTANCE;
}
....
}
对于不需要维护WriteQueue和AccessQueue的配|(x有expire time或size limit的evict{略Q来_我们可以使用DISCARDING_QUEUE以节省内存:
static final Queue<? extends Object> DISCARDING_QUEUE = new AbstractQueue<Object>() {
@Override
public boolean offer(Object o) {
return true;
}
@Override
public Object peek() {
return null;
}
@Override
public Object poll() {
return null;
}
....
};
Segment中的evict
在解决了所有数据结构的问题以后Q让我们来看看LocalCache中的核心cSegment的实玎ͼ首先从evict开始。在Guava Cache的evict时机上,它没有用另一个后台线E每隔一D|间扫瞄一ơtable以evict那些已经expire的entry。而是它在每次操作开始和l束时才做一遍清理工作,q样可以减少开销Q但是如果长旉不调用方法的话,会引h些entry不能及时被evict出去。evict主要处理四个QueueQ?. keyReferenceQueueQ?. valueReferenceQueueQ?. writeQueueQ?. accessQueue。前两个queue是因为WeakReference、SoftReference被垃圑֛收时加入的,清理时只需要遍历整个queueQ将对应的项从LocalCache中移除即可,q里keyReferenceQueue存放ReferenceEntryQ而valueReferenceQueue存放的是ValueReferenceQ要从LocalCache中移除需要有keyQ因而ValueReference需要有对ReferenceEntry的引用。这里的U除通过LocalCache而不是Segment是因为在U除时因为expandQrehashQ可能导致原来在某个Segment中的ReferenceEntry后来被移动到另一个Segment中了。而对后两个QueueQ只需要检查是否配|了相应的expire旉Q然后从头开始查扑ַlexpire的EntryQ将它们U除卛_。有不同的是在移除时Q还会注册移除的事gQ这些事件将会在接下来的操作调用注册的RemovalListener触发Q这些代码比较简单,不详q?br />在put的时候,q会清理recencyQueueQ即recencyQueue中的Entryd到accessEntry中,此时可能会发生某个Entry实际上已l被U除了,但是又被d回accessQueue中了Q这U情况下Q如果没有用WeakReference、SoftReferenceQ也没有配置expire旉Q则会引起一些内存泄漏问题。recencyQueue在get操作时被dQ但是ؓ什么会有这个Queue的存在一直没有想明白?br />
Segment中的put操作
put操作相对比较单,首先它需要获得锁Q然后尝试做一些清理工作,接下来的逻辑cMConcurrentHashMap中的rehashQ不详述。需要说明的是当扑ֈ一个已存在的EntryӞ需要先判断当前的ValueRefernece中的g实上已经被回收了Q因为它们可以时WeakReference、SoftReferencecdQ如果已l被回收了,则将新值写入。ƈ且在每次更新时注册当前操作引LU除事gQ指定相应的原因QCOLLECTED、REPLACED{,q些注册的事件在退出的时候统一调用LocalCache注册的RemovalListenerQ由于事件处理可能会有很长时_因而这里将事g处理的逻辑在退出锁以后才做。最后,在更新已存在的Entryl束后都试着那些已lexpire的EntryU除。另外put操作中还需要更新writeQueue和accessQueue的语义正性?br /> V put(K key, int hash, V value, boolean onlyIfAbsent) {
....
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();
if (entryValue == null) {
++modCount;
if (valueReference.isActive()) {
enqueueNotification(key, hash, valueReference, RemovalCause.COLLECTED);
setValue(e, key, value, now);
newCount = this.count; // count remains unchanged
} else {
setValue(e, key, value, now);
newCount = this.count + 1;
}
this.count = newCount; // write-volatile
evictEntries();
return null;
} else if (onlyIfAbsent) {
recordLockedRead(e, now);
return entryValue;
} else {
++modCount;
enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED);
setValue(e, key, value, now);
evictEntries();
return entryValue;
}
}
}
...
} finally {
...
postWriteCleanup();
}
}
Segment带CacheLoader的get操作
q部分的代码有点不知道怎么说了Q大概上的步骤是Q?. 先查找table中是否已存在没有被回收、也没有expire的entryQ如果找刎ͼq在CacheBuilder中配|了refreshAfterWriteQƈ且当前时间间隔已l操作这个事Ӟ则重新加载|否则Q直接返回原有的|2. 如果查找到的ValueReference是LoadingValueReferenceQ则{待该LoadingValueReference加蝲l束Qƈq回加蝲的|3. 如果没有扑ֈentryQ或者找到的entry的gؓnullQ则加锁后,l箋table中已存在key对应的entryQ如果找到ƈ且对应的entry.isLoading()为trueQ则表示有另一个线E正在加载,因而等待那个线E加载完成,如果扑ֈ一个非null|q回该|否则创徏一个LoadingValueReferenceQƈ调用loadSync加蝲相应的|在加载完成后Q将新加载的值更新到table中,卛_部分情况下替换原来的LoadingValueReference?br />
Segment中的其他操作
其他操作包括不含CacheLoader的get、containsKey、containsValue、replace{操作逻辑重复性很大,而且和ConcurrentHashMap的实现方式也cMQ不在详q?br />
Cache StatsCounter和CacheStats
ZU录Cache的用情况,如果命中ơ数、没有命中次数、evictơ数{,Guava Cache中定义了StatsCounter做这些统计信息,它有一个简单的SimpleStatsCounter实现Q我们也可以通过CacheBuilder配置自己的StatsCounter?br /> public interface StatsCounter {
public void recordHits(int count);
public void recordMisses(int count);
public void recordLoadSuccess(long loadTime);
public void recordLoadException(long loadTime);
public void recordEviction();
public CacheStats snapshot();
}
在得到StatsCounter实例后,可以使用CacheStats获取具体的统计信息:
public final class CacheStats {
private final long hitCount;
private final long missCount;
private final long loadSuccessCount;
private final long loadExceptionCount;
private final long totalLoadTime;
private final long evictionCount;

}
同ConcurrentHashMapQ在知道Segment实现以后Q其他的Ҏ基本上都是代理给Segment内部ҎQ因而在LocalCachecM的其他方法看h比较容易理解,不在详述。然而Guava Cacheq没有将ConcurrentMap直接提供l用户用,而是Z区分Cache和MapQ它自定义了一个自qCache接口和LoadingCache接口Q我们可以通过CacheBuilder配置不同的参敎ͼ然后使用build()Ҏq回一个Cache或LoadingCache实例Q?br />public interface Cache<K, V> {
V getIfPresent(Object key);
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
void put(K key, V value);
void putAll(Map<? extends K,? extends V> m);
void invalidate(Object key);
void invalidateAll(Iterable<?> keys);
void invalidateAll();
long size();
CacheStats stats();
ConcurrentMap<K, V> asMap();
void cleanUp();
}
public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
V get(K key) throws ExecutionException;
V getUnchecked(K key);
ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;
V apply(K key);
void refresh(K key);
ConcurrentMap<K, V> asMap();
}

]]>