WeakCache是由java.lang.reflect反射包下提供的二级缓存。二级缓存的结构为
final class WeakCache<K, P, V> {
...
}
如上代码所示,WeakCache使用关键字final修饰,不允许被继承。并且会声明三种泛型参数,K为Key的类型,P为参数的类型,V为最终结果的类型。
WeakCache共有5个私有属性,分别为:
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
// the keytypeis Object for supporting null key
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
= new ConcurrentHashMap<>();
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap
= new ConcurrentHashMap<>();
private final BiFunction<K, P, ?> subKeyFactory;
private final BiFunction<K, P, V> valueFactory;
WeakCache类仅提供了一个构造方法:
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
构造方法入参需提供两个BiFunction类型的非null函数式实例,其中一个subKeyFactory用于根据K类型参数、P类型参数生成二级Key,另外一个valueFactory用于根据K类型参数和P类型参数生成V类型的具体值,即Value。
WeakCache内部声明了5个私有内部类,其中1个内部接口,1个非静态内部类,3个静态内部类。
/**
* Common type of value suppliers that are holding a referent.
* The {@link #equals} and {@link #hashCode} of implementations is defined
* to compare the referent by identity.
*/
private interface Value<V> extends Supplier<V> {}
这里定义了WeakCache中二级缓存的value的类型(Value
因此需要自定义Value继承Supplier,任何实现Value接口都应该重写hashCode()及equals()方法。下面会说到CacheValue、LookupValue都会实现Value接口,也都重写两个方法。
private final class Factory implements Supplier<V> {
...
}
Factory作为私有内部类,实现了Supplier
Factory共有4个私有属性,分别为:
private final K key;
private final P parameter;
private final Object subKey;
private final ConcurrentMap<Object, Supplier<V>> valuesMap;
对于二级缓存的Value来说,最终要的四个属性包括:
这四个属性均为Factory构建value的必要参数,因此Factory仅提供了一个构造函数,形参需传入这四个属性。
Factory仅提供了一个实现Supplier接口的get方法,主要是用于根据以上四个缓存的属性延迟获取真正的value对象。代码及注释贴下:
public synchronized V get() { // 序列化进入,防止并发
// 按照惯例,加锁内部重新检查[锁等待期间可能情况不一样了]
Supplier<V> supplier = valuesMap.get(subKey);
if (supplier != this) {
// 检查失败,缓存内部二级Key对应的Value已经不是当前的Factory对象了。后续无法继续处理,只能返回null,由调用方法处理这种内部检查不符合预期的情况。
// 可能我们将Factory值替换为了CacheValue值了,返回null可以直接取cacheValue即可
// 也可能是valueFactory根据K\P生成value时出现异常或null,此时的Factory对象会被认为为无效值。结果从二级缓存中移除并且抛异常,下次在进入get时,就可能会为null。
return null;
}
// else still us (supplier == this)
// 校验通过后,根据this进行获取V类型的Value值
V value = null;
try {
// 使用valueFactory根据K和P来生成Value
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
if (value == null) { // 生成的Value为null时,必须把当前Factory对象缓存移除掉
valuesMap.remove(subKey, this);
}
}
// 必须保证valueFactory生成的Value不会null
assert value != null;
// 对生成的value使用内部类CacheValue封装,CacheValue是继承了WeakReference,也属于弱引用,后续会单独提到CacheValue。
CacheValue<V> cacheValue = new CacheValue<>(value);
// 将cacheValue放置在倒排Map中,方便后续判断
reverseMap.put(cacheValue, Boolean.TRUE);
// 使用cacheValue替换二级Key对应的缓存值
if (!valuesMap.replace(subKey, this, cacheValue)) {
// 细节,调用底层方法时,返回值各种情况都应该考虑到
throw new AssertionError("Should not reach here");
}
// 返回真实的V类型的value值
return value;
}
JDK的代码都是由很多性能优化及细节值得我们学习的,所以看源码并不能止步于看懂,而是真正理解,理解后知道我们下次碰见类似的情况是否也能这样思考,是否也能注意容易忽略的细节部分。下面根据一些问题我们展开思考:
为什么"throw new AssertionError(“Should not reach here”)"不清除缓存?
这种情况是绝对的error,valueFactory获取value出现异常会清除重试
private static final class CacheValue<V>
extends WeakReference<V> implements Value<V> {
...
}
WeakCache声明了私有静态内部类CacheValue,是用于封装缓存value的类型。CacheValue继承了WeakReference,因此类CacheValue包装的数据均为弱引用,生命周期为下一次GC之前。CacheValue实现了Value接口,但是接口内get()方法实际上是由WeakReference继承实现的。
CacheValue判等逻辑为“==”操作。hashCode()返回内存地址,equals()返回逻辑有两点:① CacheValue对象==;② 均为Value内部类型且Value#get()返回类型V的实际对象==。
疑问:似乎CacheValue不实现Value接口也能行?确实,但是Value接口的含义在于重写hashcode()与equals()方法。
private static final class CacheKey<K> extends WeakReference<K> {
...
}
CacheKey也是私有静态内部类,同样也继承了WeakReference,因此使用CacheKey是为了包装K类型的Key为弱引用类型,生命周期为下一次GC之前。
CacheKey和CacheValue还有三点主要差异:(不算CacheValue实现了Value接口)
CacheKey的equals()方法中使用this.getClass判断类对象,其实可以替换为“obj instance CacheKey”。但是一般instance用于判断基类引用的真实类型,这里CacheKey类为final不存在继承、多态。可能这里使用instance有点奇怪吧。
private static final class LookupValue<V> implements Value<V> {
...
}
LookupValue也是私有静态内部类,实现了Value接口。从LookupValue类名上看,是查找Value的意思。其实这就是为了方便从倒排Map(reverseMap)中查询Value。前面讲过,reverseMap查找Value的hash不能使用Supplier默认方法(继承自Object),而应该实现Value接口重写hashcode()与equals()方法。
为什么不复用CacheValue而是新增LookupValue呢?查找Value是否存在仅是为了查找而不会存储,因此没必要用弱引用包装,LookupValue仅实现Value接口,性能肯定更好。
WeakCache共提供了3个公开方法:
所有的公开方法被访问时都会调用私有方法expungeStaleEntries()去根据WeakCache#refQueue队列poll出已清除的CacheKey来清空WeakCache#map、WeakCache#reverseMap中的无效缓存引用。
这里仅给出get方法的代码解析,其他两个代码简单,不再赘述。
public V get(K key, P parameter) {
Objects.requireNonNull(parameter); // 例行校验P类型参数不能为null
expungeStaleEntries(); // 公开方法均会先清楚已回收的Key及缓存
// 使用CacheKey创建(一级)Key的弱引用,并且使用了引用队列。
// 【之所以返回Object,兼容key为null的情况】
Object cacheKey = CacheKey.valueOf(key, refQueue);
// 根据cacheKey获取二级map-valuesMap。
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {
// 注意:这里可以仅使用下面一块代码,提前get增加性能优化。
// cacheKey不存在,这里必须使用putIfAbsent,而不能使用put进行初始化
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) { // putIfabsent存在返回已有值,不存在返回null
valuesMap = oldValuesMap;
}
}
// create subKey and retrieve the possible Supplier stored by that
// subKey from valuesMap
// 一级上面处理完了,下面就是处理二级map-valuesMap的过程。
// 首先获取二级Key-subKey。二级Key的生成逻辑由调用方提供的subKeyFactory、K、P决定。
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 从二级map中尝试获取subKey的值,这里的值可能是null也可能是Factory也可能是CacheValue
// 注意:supplier即便不为null,也可能get()==null,原因是GC已经回收了
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null; // 声明缓存Value的工厂前身(见Factory类解析)
while (true) {
if (supplier != null) {
// supplier可能是Factory也可能是CacheValue
// 注意:supplier即便不为null,也可能get()==null,原因是GC已经回收了
V value = supplier.get();
if (value != null) {
// 不为null,说明通过Factory或CacheValue获得了value,直接返回
return value;
}
}
/**
* 三种情况:
* ① 二级缓存map中没有subKey对应的值,即supplier==null 【唯第一次循环】
* ② supplier从二级缓存map中获取不为Null,但由于GC回收,get()==null 【不确定】
* ③ supplier instanceof Factory 且 Factory.get()在异常时会返回null。【非第一次循环】
*/
if (factory == null) {
// 初始化Factory对象,包含value创建的所有必要数据 【唯第一次循环】
factory = new Factory(key, parameter, subKey, valuesMap);
}
if (supplier == null) {
// supplier为null,尝试向二级map中添加【唯第一次循环】
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
// 成功向二级map添加factory
supplier = factory;
}
// 多线程原因,二级map中已经有了subKey的supplier。
// 同样的,这里虽不为null,也并不代表它不会是已GC回收的弱引用
} else {
/**
* supplier不为null,有两种可能:
* ① supplier从二级缓存map中获取不为Null,但由于GC回收,get()==null 【不确定】
* ② supplier instanceof Factory 且 Factory.get()在异常时会返回null。【非第一次循环】
*/
if (valuesMap.replace(subKey, supplier, factory)) {
// 这里是情况①,不会是②,Factory.get()在异常时会在finally中从二级map中清除
// supplier因为被回收,只能重置为factory
supplier = factory;
} else {
// Factory.get()异常,重试
supplier = valuesMap.get(subKey);
}
}
}
}
上面方法注释的十分详细了,这里就简要说下几个部分:
(1)代码逻辑中设置map数据均使用的是putIfAbsent方法,不能使用put方法。在处理一级Key使用putIfAbsent之前还使用了get方法判断是否存在二级map。这里使用get()方法也是为了优化性能。
(2)在处理二级map获取value的过程,注意三种情况:① supplier == null ② supplier不为null 但已被回收 ③ supplier不为null、未被回收,但内部出错重试。
(3) 从获取缓存value的逻辑来看,Factory的封装性更加重要,反而延迟性(节省内存,避免不必要的初始化)实际上体现的并不明显。由于Factory将value初始化所需要的数据封装起来,赋值给supplier进行兜底(如null或已被回收),转换后统一使用supplier#get获取value值。
WeakCache的基本内容和代码都讲解完了,为了更好的认知WeakCache的使用场景,还有几个问题需要我们明确并学习下。
在大部分的延迟缓存的设计中,是需要把Factory对象作为缓存值看待,真正需要的时候,再将Factory转换为实际可用value。
WeakCache是由java.lang.reflect反射包下提供的二级弱缓存类,其主要的应用场景即使JDK动态代理。在JDK动态代理中,用于缓存已经生成的代理类的class对象。其中类型K为ClassLoader(类加载器),P类型为Class>[](接口列表),V类型为Class>(代理类class对象)。
WeakCache需要两个构造参数subKeyFactory、valueFactory,分别用于构建sub-Key和最终值。因此,JDK动态代理的一级Key就是类加载器实例,二级Key由类加载器和接口列表构成(实际上,只与接口列表相关),最终代理类class对象是由valueFactory生成。