Guava缓存器的数据查询函数调用:
get(K) ——
localCache.getOrLoad(key) ——
get(key, defaultLoader)(如果创建的为Cache缓存,defaultLoader设为null)
具体步骤:
1、计算key的哈希值。
LocalCache会使用分段锁来保护不同段的数据,在查询或插入元素时,需要通过哈希算法定位到Segment, LocalCache 会使用变种Hash算法Wang/Jenkins 对元素的hashCode进行一次再哈希 :
static int rehash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
再哈希的目的是为了减少哈希冲突,使元素能够比较均匀的分布在不同的Segment上,从而提高缓存器的存取效率。如果哈希的质量太差,元素集中哈希到少数Segment中,不仅存取元素缓慢,分段锁也会失去意义。
2、通过哈希值,获取数据段。
segments[(hash >>> segmentShift) & segmentMask]
下面分析下初始化segmentShift、segmentMask及segments的代码:
//缓存器支持的最大段数MAX_SEGMENTS 为1 << 16,如果在builder中没有设置concurrencyLevel的大小,默认为4。
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
//一般计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度,如果设置了maxWeight,segmentCount 还需满足segmentCount * 20 <= maxWeight ,此时假设每个段平均哈希到20个元素,再少就不继续增加段了:
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel
&& (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
++segmentShift;
segmentCount <<= 1;
}
this.segmentShift = 32 - segmentShift;
segmentMask = segmentCount - 1;
this.segments = newSegmentArray(segmentCount);
假如concurrencyLevel等于14或15, segmentCount 等于16,则缓存器里锁的个数也是16。
3、调用数据段的get方法。
V get(K key, int hash, CacheLoader<? super K, V> loader)
1)获取键值的引用: ReferenceEntry<K, V> e = getEntry(key, hash);
获取值:V value = getLiveValue(e, now);
其中判断数据是否过期的方法:
boolean isExpired(ReferenceEntry<K, V> entry, long now) {
checkNotNull(entry);
if (expiresAfterAccess()
&& (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
return true;
}
if (expiresAfterWrite()
&& (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
return true;
}
return false;
}
如果获取的值不为空,则需要将此次访问加入到访问队列(Queue<ReferenceEntry<K, V>>)中,当缓存器满时,用来选择需要清出的数据,同时记录命中信息。
如果获取的值正在加载(valueReference.isLoading()),则需等待其加载完后返回。
2)键值引用为空,则需要加载。
lockedGetOrLoad(key, hash, loader);
数据的加载过程会上ReentrantLock锁(Segment继承至ReentrantLock),同时会重新进行验证:如果缓存值正在加载,则无需进行下一步操作;若果缓存值没在加载,且缓存值已存在于缓存且没有过期,则返回该值;验证完才开始同步加载。
Guava缓存器将缓存数据分为一个个段,每个段中的数据实际存放在volatile AtomicReferenceArray<ReferenceEntry<K, V>> table中, ReferenceEntry接口中定义了许多与键值相关的函数,比如获取访问时间、写时间、键、值等, Guava缓存器在数据查询时,考虑得很全面,以后遇到多线程环境下数据读写的问题,可以借鉴这里面的思想。