WeakCache二级缓存

WeakCache弱缓存略析

  • 一、WeakCache类总览
  • 二、WeakCache属性
  • 三、WeakCache构造方法
  • 四、WeakCache的内部类
    • 4.1 Value接口
    • 4.2 Factory内部类
      • 4.2.1 Factory属性
      • 4.2.1 Factory公开方法
    • 4.3 CacheValue内部类
    • 4.4 CacheKey内部类
    • 4.5 LookupValue内部类
  • 五、WeakCache的公开方法
  • 六、提问
  • 七、使用场景
  • 【参考资料】

一、WeakCache类总览

WeakCache是由java.lang.reflect反射包下提供的二级缓存。二级缓存的结构为,其中Key和Value均为弱引用,而Sub-Key为强引用。获取缓存值的get()方法时不仅需要Key,还需要多添加一个参数(P类型)。通过这个参数和Key就可以计算出Sub-Key。WeakCache对象的构造方法中会传入两个工厂对象,其中一个为subKeyFactory用于计算Sub-Key的,另外一个为valueFactory用于计算最终的结果的。Key可以为Null但是Sub-key和Value不能为Null。弱引用对象被GC回收掉后不会立马清除缓存,而是会在WeakCache的公开方法方法中手动懒式清除。

final class WeakCache<K, P, V> {
...
}

如上代码所示,WeakCache使用关键字final修饰,不允许被继承。并且会声明三种泛型参数,K为Key的类型,P为参数的类型,V为最终结果的类型。

二、WeakCache属性

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;
  • refQueue: 用于存放GC后被回收的一级Key(使用WeakReference包装)。Reference相关资料
  • map:由嵌套ConcurentMap构成的二级缓存,其中第一层Key由K类型参数生成,第二层Key有K类型参数及P类型参数共同计算的结果。注意这里的value的实际类型可能会有多种情况。
  • reverseMap:由于两层缓存,因此通过倒排Map加快判断某value结果是否已经存在。
  • subKeyFactory:BiFunction类型,根据K类型参数及P类型参数构造二级Key
  • valueFactory:BiFunctionle类型,根据K类型参数及P类型参数获取Value.

三、WeakCache构造方法

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的内部类

WeakCache内部声明了5个私有内部类,其中1个内部接口,1个非静态内部类,3个静态内部类。

4.1 Value接口

    /**
     * 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),实际上是Supplier类型。为什么不直接使用Supplier呢?在前面WeakCache属性中我们提到reverseMap适用于倒排Map查找缓存值使用,但是reverseMap的类型为ConcurrentMap, Boolean>。这问题就来了,在这个map进行hash时是按照Supplier实例进行的,但是Supplier实际上仅是我们对V类型值的延迟封装,真正hash应该按照value值进行,即我们不关心Supplier对value封装的具体形式,只要是supplier1.get()== supplier2.get()也认为是同一个value。
因此需要自定义Value继承Supplier,任何实现Value接口都应该重写hashCode()及equals()方法。下面会说到CacheValue、LookupValue都会实现Value接口,也都重写两个方法。

4.2 Factory内部类

private final class Factory implements Supplier<V> {
...
}

Factory作为私有内部类,实现了Supplier的接口。在WeakCache中的主要作用是完成了对结果Value生成过程、延迟构建的封装。二级缓存的更新也会由Factory完成。

4.2.1 Factory属性

Factory共有4个私有属性,分别为:

private final K key;
private final P parameter;
private final Object subKey;
private final ConcurrentMap<Object, Supplier<V>> valuesMap;

对于二级缓存的Value来说,最终要的四个属性包括:

  1. key:K类型的一级Key
  2. parameter:P类型的参数
  3. subKey:由以上二者生成后的二级Key
  4. valuesMap:当前一级Key下所有的缓存Map

这四个属性均为Factory构建value的必要参数,因此Factory仅提供了一个构造函数,形参需传入这四个属性。

4.2.1 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的代码都是由很多性能优化及细节值得我们学习的,所以看源码并不能止步于看懂,而是真正理解,理解后知道我们下次碰见类似的情况是否也能这样思考,是否也能注意容易忽略的细节部分。下面根据一些问题我们展开思考:

  1. 根据Factory#get()方法内部都做了些什么?
    主要负责使用valueFactory根据K和P来获取Value的实际逻辑。并且获取到value后会把对应的值重新封装为CacheValue弱引用缓存起来,在下次GC之前获取Value就可以通过缓存了。当然,此外还会把CacheValue放入倒排Map中便于后续判断缓存是否存在指定的value值。
  2. Factory为什么是内部类而不是静态内部类?
    Factory对象get()方法内部需要使用到外部类非静态属性-valueFactory;
  3. WeakCache为什么不直接调用valueFactory生成Value,而要设计Factory内部类?
    第一个原因是封装性好。将valueFactory生成Value的逻辑以及将value包装为弱引用,且记录倒排map这些一气呵成的逻辑封装起来。
    第二个原因-延迟获取缓存值,节省内存,提升速度。valueFactory是外界提供的,生成value实际大小不明确。WeakCache#map缓存值使用的策略是先使用用Factory对象占坑(Factory对象创建时仅缓存所需要的数据即可,内存占用小),真正需要获取value时才使用Factory#get()方法获取实际的Value。获取真正value之后,会把value封装为弱引用(CacheValue)替换掉Factory对象。这样做,既满足了缓存的需求,又不会浪费内存空间。【在很多底层设计中都有类似的操作,一定要学会】
  4. Factory#get()方法为什么要使用synchronized关键字?
    主要是为了保证缓存map(WeakCache#map)和倒排map(WeakCache#reverseMap)操作的原子性(or,一致性)。单个map都是concurrentMap类型,get\put操作均是线程安全的。但是Factory#get()获取到value之后会不会存在reverseMap有对应的cacheValue,而在valuesMap#replace覆盖不成功呢?在无synchronized时是有可能的,假设存在2个线程同是执行get,其中一个线程执行至reverseMap#put(),另外一个线程执行valueFactory#apply异常,就会存在这种问题。
  5. 同步代码块应注意哪些问题?
    在synchronized同步代码块中一定要进行二次检查,二次检查主要是为了防止等待锁的过程中,外部数据的变化与同步代码逻辑不兼容性。比如在单例模式(DCL)中,也会二次检查对象是否已经创建-已经创建了就不能再创建了,即不必执行同步代码。在Factory#get()方法中检查二级缓存的值是否为当前Factory对象,校验成功后使用Factory对象执行下面同步代码逻辑。需要注意的是,同步代码块正常执行或任何异常都必须保证该检查条件成立。正常执行后会将缓存中value替换为CacheValue,valueFactory获取value出现异常后会将二级缓存清除。

为什么"throw new AssertionError(“Should not reach here”)"不清除缓存?
这种情况是绝对的error,valueFactory获取value出现异常会清除重试

4.3 CacheValue内部类

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()方法。

4.4 CacheKey内部类

private static final class CacheKey<K> extends WeakReference<K> {
...
}

CacheKey也是私有静态内部类,同样也继承了WeakReference,因此使用CacheKey是为了包装K类型的Key为弱引用类型,生命周期为下一次GC之前。
CacheKey和CacheValue还有三点主要差异:(不算CacheValue实现了Value接口)

  1. CacheKey允许Key为null
    当Key为null时,使用NULL_KEY空对象代替。
  2. CacheKey使用了ReferenceQueue,当GC回收后,可以通过引用队列手动清楚掉。(为啥手动清楚?为了清空WeakCache#map、WeakCache#reverseMap)
  3. CacheKey还提供了expungeFrom方法。手动清空ReferenceQueue时,会清空WeakCache#map、WeakCache#reverseMap。

CacheKey的equals()方法中使用this.getClass判断类对象,其实可以替换为“obj instance CacheKey”。但是一般instance用于判断基类引用的真实类型,这里CacheKey类为final不存在继承、多态。可能这里使用instance有点奇怪吧。

4.5 LookupValue内部类

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的公开方法

WeakCache共提供了3个公开方法:

  • get(K key, P parameter):根据K类型Key值即P类型参数来查找缓存的Value值。
  • containsValue(V value):判断缓存中是否包含某个Vlaue
  • size():返回所有缓存的Value值。

所有的公开方法被访问时都会调用私有方法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的使用场景,还有几个问题需要我们明确并学习下。

  1. 为什么WeakCache的key、value设置为弱引用,而sub-Key为强引用?
    缓存值value设置弱引用的的目的是控制缓存的有效期,至下一个GC之前。
    缓存一级key也设置为弱引用是为了跟踪缓存值回收后记录到队列中,手动清除各map中的引用。
    sub-Key不需要弱引用,手动清楚引用时,二级key也会被删除。
  2. 为什么WeakCache一级key允许为null,而sub-Key、value均不允许为null
    value在WeakCache中设计为弱引用,当value为null的时候就会引起歧义,即value是已经被回收了还是本身就是null,无法区分。
    sub-Key是由使用方提供的subKeyFactory以及一级Key、P类型参数共同生成的。subKeyFactory返回的null谁也不知道是异常还是正常null值,可能后引起非常难排查的bug。【不信任调用方,那就不支持,干脆直接约定不能为Null】
    一级key是确定的,由调用方直接传过来的(不含任何中间逻辑),不会出现sub-Key的歧义问题。
    Tip:支持或不支持null,很多时候都是因为歧义性风险的取舍
  3. 为什么Value实现类型需要重写hashcode()及equals()?
    缓存的value值是通过弱引用类型封装的,真实value是其内部get()获取的值。因此必须重写hashcode()及equals()修改为==逻辑(包括两个部分,上面已有不再赘述)
  4. 为什么Factory不需要继承Value?
    Factory作为cacheValue的前身,为什么Factory不实现接口Value呢?如果Factory作为延迟构建性,我想Factory应该也会实现接口Value,并重写hashCode与equals(),但其实不然,虽然Factory对象会被暂时性的存储在WeakCache#map中,但是下一步就是会把Factory对象转换为CacheValue,所以Factory仅是构建value过程和数据的封装,并不是真正的缓存值value。
    同时可以看到,WeakCache#size()方法统计的个数视为CacheValue的个数,而不包含Factory对象。所以Factory不需要重写hashCode与equals(),Factory也不会通过WeakCache#map中get()获取到。

在大部分的延迟缓存的设计中,是需要把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生成。

【参考资料】

  1. 知乎-WeakCache介绍
  2. 一文点破WeakCache缓存那点事儿

你可能感兴趣的:(Java,java,后端,proxy模式)