Mybatis

第07篇:Mybatis缓存装饰器

2026-01-299 min readJava 专题系列
Mybatis

!!!tip MyBatis 对缓存的设计是非常巧妙的。花样很多,但却不是真的花样。因为Mybatis只是对 Map数据结构的封装, 但是却实现了很多挺好用的能力。 如果单单从设计模式上的角度来,其实就是典型的装饰器模式, 装饰器模式其实并不难,所以我们不讲设计模式, 本篇文章我们来看看Mybatils 缓存设计巧妙的点。 !!!

官方文档

下面通过简单的代码review来分析下这11个缓存类设计的巧妙点。(因为是对博客重构,历史图片就没有补充,图上只有10个,请讲究下)


一、模式分析

从目录就很清晰看出,核心就是impl 包下面只有一个,其他都是装饰器模式,在 decorators 包下

!!!tip

其实上面就是Mybatis 关于 Cache 的核心实现,其实看到这里还没有很多知识点. 那么我们从中能学到什么呢? 如果真要找一条学习的点,那么就是:

设计要面向接口设计,而不是具体实现。 这样当我们要重写 Cache ,比如说我们不想底层用 HashMap 来实现了,其实我们只要实现一下 Cache 接口,然后替换掉PerpetualCache就可以了。对于使用者其实并不感知。

!!!

1.1 Cache

接口设计没有什么好讲的,提供获取和添加方法,跟Map接口一样。 本篇我们要一起Review的类都会实现该接口的。

(这句话简直就是废话,大佬勿喷,就是简单提醒。意思就是其实代码不难)

java
1public interface Cache { 2 3 String getId(); 4 5 void putObject(Object key, Object value); 6 7 Object getObject(Object key); 8 9 Object removeObject(Object key); 10 11 void clear(); 12 13 int getSize(); 14 15 ReadWriteLock getReadWriteLock(); 16 17}

1.2 PerpetualCache

这个类就是 Mybatis 缓存最底层的设计, 看一下就知道其实是对 Map 的封装。 其实我们只要知道他是简单的 HashMap 的封装就可以了。因为代码实战是太简单了,没啥分析的。

java
1public class PerpetualCache implements Cache { 2 // 唯一标识 3 private final String id; 4 // 就是一个HashMap结构 5 private Map<Object, Object> cache = new HashMap<Object, Object>(); 6 7 public PerpetualCache(String id) { 8 this.id = id; 9 } 10 11 @Override 12 public String getId() { 13 return id; 14 } 15 16 @Override 17 public int getSize() { 18 return cache.size(); 19 } 20 21 @Override 22 public void putObject(Object key, Object value) { 23 cache.put(key, value); 24 } 25 26 @Override 27 public Object getObject(Object key) { 28 return cache.get(key); 29 } 30 31 @Override 32 public Object removeObject(Object key) { 33 return cache.remove(key); 34 } 35 36 @Override 37 public void clear() { 38 cache.clear(); 39 } 40 // 基本没啥用,外层谁要用,谁重写 41 @Override 42 public ReadWriteLock getReadWriteLock() { 43 return null; 44 } 45 46 @Override 47 public boolean equals(Object o) { 48 if (getId() == null) { 49 throw new CacheException("Cache instances require an ID."); 50 } 51 if (this == o) { 52 return true; 53 } 54 if (!(o instanceof Cache)) { 55 return false; 56 } 57 58 Cache otherCache = (Cache) o; 59 return getId().equals(otherCache.getId()); 60 } 61 62 @Override 63 public int hashCode() { 64 if (getId() == null) { 65 throw new CacheException("Cache instances require an ID."); 66 } 67 return getId().hashCode(); 68 } 69 70} 71

二、开始重头戏

从这里我们主要一起看下,代码设计的巧妙之处,一个一个研究下,以下这10个类。看 Mybatis 是如何巧妙设计的。

2.1 BlockingCache

BlockingCache是一个简单和低效的Cache的装饰器,我们主要看几个重要方法。

java
1public class BlockingCache implements Cache { 2 3 private long timeout; 4 //实现Cache接口的缓存对象 5 private final Cache delegate; 6 //对每个key生成一个锁对象 7 private final ConcurrentHashMap<Object, ReentrantLock> locks; 8 9 public BlockingCache(Cache delegate) { 10 this.delegate = delegate; 11 this.locks = new ConcurrentHashMap<Object, ReentrantLock>(); 12 } 13 14 @Override 15 public String getId() { 16 return delegate.getId(); 17 } 18 19 @Override 20 public int getSize() { 21 return delegate.getSize(); 22 } 23 24 @Override 25 public void putObject(Object key, Object value) { 26 try { 27 delegate.putObject(key, value); 28 } finally { 29 //释放锁。 为什么不加锁? 所以get和put是组合使用的,当get加锁,如果没有就查询数据库然后put释放锁,然后其他线程就可以直接用缓存数据了。 30 releaseLock(key); 31 } 32 } 33 34 @Override 35 public Object getObject(Object key) { 36 //1. 当要获取一个key,首先对key进行加锁操作,如果没有锁就加一个锁,有锁就直接锁 37 acquireLock(key); 38 Object value = delegate.getObject(key); 39 if (value != null) { 40 //2. 如果缓存命中,就直接解锁 41 releaseLock(key); 42 } 43 //3. 当value=null, 就是说没有命中缓存,那么这个key就会被锁住,其他线程进来都要等待 44 return value; 45 } 46 47 @Override 48 public Object removeObject(Object key) { 49 // 移除key的时候,顺便清楚缓存key的锁对象 50 releaseLock(key); 51 return null; 52 } 53 54 @Override 55 public void clear() { 56 delegate.clear(); 57 } 58 59 @Override 60 public ReadWriteLock getReadWriteLock() { 61 return null; 62 } 63 64 private ReentrantLock getLockForKey(Object key) { 65 ReentrantLock lock = new ReentrantLock(); 66 ReentrantLock previous = locks.putIfAbsent(key, lock); 67 //如果key对应的锁存在就返回,没有就创建一个新的 68 return previous == null ? lock : previous; 69 } 70 71 private void acquireLock(Object key) { 72 Lock lock = getLockForKey(key); 73 //1. 如果设置超时时间,就可以等待timeout时间(如果超时了报错) 74 if (timeout > 0) { 75 try { 76 boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); 77 if (!acquired) { 78 throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); 79 } 80 } catch (InterruptedException e) { 81 throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); 82 } 83 } else { 84 //2. 如果没有设置,直接就加锁(如果这个锁已经被人用了,那么就一直阻塞这里。等待上一个释放锁) 85 lock.lock(); 86 } 87 } 88 89 private void releaseLock(Object key) { 90 ReentrantLock lock = locks.get(key); 91 if (lock.isHeldByCurrentThread()) { 92 lock.unlock(); 93 } 94 } 95 96 public long getTimeout() { 97 return timeout; 98 } 99 100 public void setTimeout(long timeout) { 101 this.timeout = timeout; 102 } 103}

建议看代码注释

方法解释
acquireLock加锁操作
getObject进来加锁,如果缓存存在就释放锁,不存在就不释放锁。
putObject添加元素并释放锁
removeObject移除key的时候,顺便清楚缓存key的锁对象
getLockForKey如果key对应的锁存在就返回,没有就创建一个新的

思考

  1. 这个因为每次key请求都会加lock真的会很慢吗? 我们举两种场景。

注意这个加lock并不是对get方法加lock,而是对每个要get的key来加lock。

场景一: 试想一种场景,当有10个线程同时从数据库查询一个key为123的数据时候,当第一个线程来首先从cache中读取时候,这个时候其他九个线程是会阻塞的,因为这个key已经被加lock了。当第一个线程get这个key完成时候,其他线程才能继续走。这种场景来说是不好的,

场景二: 但是当第一个线程来发现cache里面没有数据这个时候其他线程会阻塞,而第一个线程会从db中查询,然后在put到cache里面。这样其他9个线程就不需要在去查询db了,就减少了9次db查询。

2.2 FifoCache

FIFO( First Input First Output),简单说就是指先进先出

如何实现先进先出呢? 其实非常简单,当put时候,先判断是否需要执行淘汰策略,如果要执行淘汰,就 移除先进来的。 直接通过 Deque API 来实现先进先出。

java
1 private final Cache delegate; 2 private final Deque<Object> keyList; 3 private int size; 4 5 public FifoCache(Cache delegate) { 6 this.delegate = delegate; 7 this.keyList = new LinkedList<Object>(); 8 this.size = 1024; 9 } 10 11@Override 12 public void putObject(Object key, Object value) { 13 //1. put时候就判断是否需要淘汰 14 cycleKeyList(key); 15 delegate.putObject(key, value); 16 } 17 private void cycleKeyList(Object key) { 18 keyList.addLast(key); 19 //1. size默认如果大于1024就开始淘汰 20 if (keyList.size() > size) { 21 //2. 利用Deque队列移除第一个。 22 Object oldestKey = keyList.removeFirst(); 23 delegate.removeObject(oldestKey); 24 } 25 }

2.3 LoggingCache

从名字上看就是跟日志有关, LoggingCache 会在 debug级别下把缓存命中率给统计出来,然后通过日志系统打印出来。

java
1public Object getObject(Object key) { 2 requests++; 3 final Object value = delegate.getObject(key); 4 if (value != null) { 5 hits++; 6 } 7 //1. 打印缓存命中率 8 if (log.isDebugEnabled()) { 9 log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); 10 } 11 return value; 12 }

除此之外没有什么其他功能。我们主要看下他是如何统计缓存命中率的。其实很简单。

java
1public class LoggingCache implements Cache { 2 3 private final Log log; 4 private final Cache delegate; 5 //1. 总请求次数 6 protected int requests = 0; 7 //2. 命中次数 8 protected int hits = 0; 9 10 ... 11}

在get请求时候无论是否命中,都自增总请求次数( request ), 当get命中时候自增命中次数( hits )

java
1public Object getObject(Object key) { 2 //1. 无论是否命中,都自增总请求次数( `request` ) 3 requests++; 4 final Object value = delegate.getObject(key); 5 if (value != null) { 6 //2. get命中时候自增命中次数( `hits` ) 7 hits++; 8 } 9 if (log.isDebugEnabled()) { 10 log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); 11 } 12 return value; 13 }

然后我们看命中率怎么算 getHitRatio()

命中率 = 命中次数 / 总请求次数

java
1 private double getHitRatio() { 2 return (double) hits / (double) requests; 3 }

2.4 LruCache

LRU是Least Recently Used的缩写,即最近最少使用。

首先我们看如何实现 LRU 策略。 它其实就是利用 LinkedHashMap来实现 LRU 策略, JDK 提供的 LinkedHashMap天然就支持 LRU 策略。 LinkedHashMap 有一个特点如果开启LRU策略后,每次获取到数据后,都会把数据放到最后一个节点,这样第一个节点肯定是最近最少用的元素。

java
1public V get(Object key) { 2 Node<K,V> e; 3 if ((e = getNode(hash(key), key)) == null) 4 return null; 5 //1. 判断是否开始LRU策略 6 if (accessOrder) 7 //2. 开启就往后面放 8 afterNodeAccess(e); 9 return e.value; 10 }

在这里插入图片描述 构造中先声明LRU淘汰策略,当size()大于构造中声明的1024就可以在每次 putObject时候将要淘汰的移除掉。这点非常的巧妙,不知道你学习到了没 ?

在这里插入图片描述

2.5 ScheduledCache

定时删除,设计巧妙,可以借鉴。

java
1public class ScheduledCache implements Cache { 2 3 private final Cache delegate; 4 protected long clearInterval; 5 protected long lastClear; 6 7 public ScheduledCache(Cache delegate) { 8 this.delegate = delegate; 9 //1. 指定多久清理一次缓存 10 this.clearInterval = 60 * 60 * 1000; // 1 hour 11 //2. 设置初始值 12 this.lastClear = System.currentTimeMillis(); 13 } 14 15 public void setClearInterval(long clearInterval) { 16 this.clearInterval = clearInterval; 17 } 18 19 @Override 20 public String getId() { 21 return delegate.getId(); 22 } 23 24 @Override 25 public int getSize() { 26 clearWhenStale(); 27 return delegate.getSize(); 28 } 29 30 @Override 31 public void putObject(Object key, Object object) { 32 clearWhenStale(); 33 delegate.putObject(key, object); 34 } 35 36 @Override 37 public Object getObject(Object key) { 38 return clearWhenStale() ? null : delegate.getObject(key); 39 } 40 41 @Override 42 public Object removeObject(Object key) { 43 clearWhenStale(); 44 return delegate.removeObject(key); 45 } 46 47 @Override 48 public void clear() { 49 //1. 记录最近删除一次时间戳 50 lastClear = System.currentTimeMillis(); 51 //2. 清理掉缓存信息 52 delegate.clear(); 53 } 54 55 @Override 56 public ReadWriteLock getReadWriteLock() { 57 return null; 58 } 59 60 @Override 61 public int hashCode() { 62 return delegate.hashCode(); 63 } 64 65 @Override 66 public boolean equals(Object obj) { 67 return delegate.equals(obj); 68 } 69 70 private boolean clearWhenStale() { 71 if (System.currentTimeMillis() - lastClear > clearInterval) { 72 clear(); 73 return true; 74 } 75 return false; 76 } 77 78} 79

核心代码

  1. 构造中指定多久清理一次缓存(1小时)
  2. 设置初始值
  3. clearWhenStale() 核心方法
  4. 然后在每个方法中调用一次这段代码,判断是否需要清理。
java
1private boolean clearWhenStale() { 2 //1. 当前时间 - 最后清理时间,如果大于定时删除时间,说明要执行清理了。 3 if (System.currentTimeMillis() - lastClear > clearInterval) { 4 clear(); 5 return true; 6 } 7 return false; 8 }

2.6 SerializedCache

从名字上看就是支持序列化的缓存,那么我们就要问了,为啥要支持序列化?

为啥要支持序列化?

因为如果多个用户同时共享一个数据对象时,同时都引用这一个数据对象。如果有用户修改了这个数据对象,那么其他用户拿到的就是已经修改过的对象,这样就是出现了线程不安全。

如何解决这种问题

  1. 加锁当一个线程在操作时候,其他线程不允许操作
  2. 新生成一个对象,这样多个线程获取到的数据就不是一个对象了。

只看一下核心代码

  1. putObject 将对象序列化成byte[]
  2. getObjectbyte[]反序列化成对象
java
1public void putObject(Object key, Object object) { 2 if (object == null || object instanceof Serializable) { 3 //1. 将对象序列化成byte[] 4 delegate.putObject(key, serialize((Serializable) object)); 5 } else { 6 throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object); 7 } 8 } 9private byte[] serialize(Serializable value) { 10 try { 11 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 12 ObjectOutputStream oos = new ObjectOutputStream(bos); 13 oos.writeObject(value); 14 oos.flush(); 15 oos.close(); 16 return bos.toByteArray(); 17 } catch (Exception e) { 18 throw new CacheException("Error serializing object. Cause: " + e, e); 19 } 20 } 21 22 public Object getObject(Object key) { 23 Object object = delegate.getObject(key); 24 //1. 获取时候将byte[]反序列化成对象 25 return object == null ? null : deserialize((byte[]) object); 26 } 27 private Serializable deserialize(byte[] value) { 28 Serializable result; 29 try { 30 ByteArrayInputStream bis = new ByteArrayInputStream(value); 31 ObjectInputStream ois = new CustomObjectInputStream(bis); 32 result = (Serializable) ois.readObject(); 33 ois.close(); 34 } catch (Exception e) { 35 throw new CacheException("Error deserializing object. Cause: " + e, e); 36 } 37 return result; 38 }

这种就类似于深拷贝,因为简单的浅拷贝会出现线程安全问题,而这种办法,因为字节在被反序列化时,会在创建一个新的对象,这个新的对象的数据和原来对象的数据一模一样。所以说跟深拷贝一样。

Java开发之深浅拷贝

2.7 SoftCache

从名字上看,Soft其实就是软引用。软引用就是如果内存够,GC就不会清理内存,只有当内存不够用了会出现OOM时候,才开始执行GC清理。

如果要看明白这个源码首先要先了解一点垃圾回收,垃圾回收的前提是还有没有别的地方在引用这个对象了。如果没有别的地方在引用就可以回收了。 本类中为了阻止被回收所以声明了一个变量hardLinksToAvoidGarbageCollection, 也指定了一个将要被回收的垃圾队列queueOfGarbageCollectedEntries

这个类的主要内容是当缓存value已经被垃圾回收了,就自动把key也清理。

Mybatis 在实际中并没有使用这个类。

java
1public class SoftCache implements Cache { 2 private final Deque<Object> hardLinksToAvoidGarbageCollection; 3 private final ReferenceQueue<Object> queueOfGarbageCollectedEntries; 4 private final Cache delegate; 5 private int numberOfHardLinks; 6 7 public SoftCache(Cache delegate) { 8 this.delegate = delegate; 9 this.numberOfHardLinks = 256; 10 this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>(); 11 this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>(); 12 } 13}

先看下变量声明

hard Links To Avoid Garbage Collection 硬连接,避免垃圾收集 queue Of Garbage Collected Entries 垃圾要收集的队列 number Of Hard Links 硬连接数量

java
1@Override 2 public void putObject(Object key, Object value) { 3 //1. 清除已经被垃圾回收的key 4 removeGarbageCollectedItems(); 5 //2. 注意看SoftEntry(),声明一个SoftEnty对象,指定垃圾回收后要进入的队列 6 //3. 当SoftEntry中数据要被清理,会添加到类中声明的垃圾要收集的队列中 7 delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); 8 } 9 10 @Override 11 public Object getObject(Object key) { 12 Object result = null; 13 @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache 14 SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key); 15 if (softReference != null) { 16 result = softReference.get(); 17 if (result == null) { 18 //1. 如果数据已经没有了,就清理这个key 19 delegate.removeObject(key); 20 } else { 21 // See #586 (and #335) modifications need more than a read lock 22 synchronized (hardLinksToAvoidGarbageCollection) { 23 //2. 如果key存在,读取时候加一个锁操作,并将缓存值添加到硬连接集合中,避免垃圾回收 24 hardLinksToAvoidGarbageCollection.addFirst(result); 25 //3. 构造中指定硬链接最大256,所以如果已经有256个key的时候回开始删除最先添加的key 26 if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { 27 hardLinksToAvoidGarbageCollection.removeLast(); 28 } 29 } 30 } 31 } 32 return result; 33 } 34 35 @Override 36 public void clear() { 37 //执行三清 38 synchronized (hardLinksToAvoidGarbageCollection) { 39 //1.清除硬链接队列 40 hardLinksToAvoidGarbageCollection.clear(); 41 } 42 //2. 清除垃圾队列 43 removeGarbageCollectedItems(); 44 //3. 清除缓存 45 delegate.clear(); 46 } 47 48 private void removeGarbageCollectedItems() { 49 SoftEntry sv; 50 //清除value已经gc准备回收了,就就将key也清理掉 51 while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { 52 delegate.removeObject(sv.key); 53 } 54 }

2.8 SynchronizedCache

从名字看就是同步的缓存,从代码看即所有的方法都被synchronized修饰。

在这里插入图片描述

2.9 TransactionalCache

从名字上看就应该能隐隐感觉到跟事务有关,但是这个事务呢又不是数据库的那个事务。只是类似而已是, 即通过 java 代码来实现了一个暂存区域,如果事务成功就添加缓存,事务失败就回滚掉或者说就把暂存区的信息删除,不进入真正的缓存里面。 这个类是比较重要的一个类,因为所谓的二级缓存就是指这个类。既然说了🎧缓存就顺便提一下一级缓存。但是说一级缓存就设计到 Mybatis架构里面一个 Executor 执行器 在这里插入图片描述

所有的查询都先从一级缓存中查询 在这里插入图片描述

在这里插入图片描述

看到这里不由己提一个面试题,面试官会问你知道Mybatis 的一级缓存吗? 一般都会说Mybatis 的一级缓存就是 SqlSession 自带的缓存,这么说也对就是太笼统了,因为 SqlSession其实就是生成 Executor 而一级缓存就是里面query方法中的 localCache。这个时候我们就要看下了localCache 究竟是什么? 看一下构造,突然豁然开朗。原来本篇文章讲的基本就是一级缓存的实现呀。 在这里插入图片描述

说到这里感觉有点跑题了,我们不是要看 TransactionalCache 的实现吗?

clearOnCommit 为false就是这个事务已经完成了,可以从缓存中读取数据了。

clearOnCommittrue ,这个事务正在进行中呢? 来的查询都给你返回 null , 等到 commit 提交时候在查询就可以从缓存中取数据了。

java
1public class TransactionalCache implements Cache { 2 3 private static final Log log = LogFactory.getLog(TransactionalCache.class); 4 // 真正的缓存 5 private final Cache delegate; 6 // 是否清理已经提交的实物 7 private boolean clearOnCommit; 8 // 可以理解为暂存区 9 private final Map<Object, Object> entriesToAddOnCommit; 10 // 缓存中没有的key 11 private final Set<Object> entriesMissedInCache; 12 13 public TransactionalCache(Cache delegate) { 14 this.delegate = delegate; 15 this.clearOnCommit = false; 16 this.entriesToAddOnCommit = new HashMap<Object, Object>(); 17 this.entriesMissedInCache = new HashSet<Object>(); 18 } 19 20 @Override 21 public String getId() { 22 return delegate.getId(); 23 } 24 25 @Override 26 public int getSize() { 27 return delegate.getSize(); 28 } 29 30 @Override 31 public Object getObject(Object key) { 32 // 先从缓存中拿数据 33 Object object = delegate.getObject(key); 34 if (object == null) { 35 // 如果没有添加到set集合中 36 entriesMissedInCache.add(key); 37 } 38 // 返回数据库的数据。 39 if (clearOnCommit) { 40 return null; 41 } else { 42 return object; 43 } 44 } 45 46 @Override 47 public ReadWriteLock getReadWriteLock() { 48 return null; 49 } 50 51 @Override 52 public void putObject(Object key, Object object) { 53 entriesToAddOnCommit.put(key, object); 54 } 55 56 @Override 57 public Object removeObject(Object key) { 58 return null; 59 } 60 61 @Override 62 public void clear() { 63 clearOnCommit = true; 64 entriesToAddOnCommit.clear(); 65 } 66 67 public void commit() { 68 if (clearOnCommit) { 69 delegate.clear(); 70 } 71 flushPendingEntries(); 72 reset(); 73 } 74 75 public void rollback() { 76 unlockMissedEntries(); 77 reset(); 78 } 79 80 private void reset() { 81 //1. 是否清除提交 82 clearOnCommit = false; 83 //2. 暂存区清理,代表这个事务从头开始做了,之前的清理掉 84 entriesToAddOnCommit.clear(); 85 //3. 同上 86 entriesMissedInCache.clear(); 87 } 88 89 /** 90 * 将暂存区的数据提交到缓存中 91 **/ 92 private void flushPendingEntries() { 93 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { 94 delegate.putObject(entry.getKey(), entry.getValue()); 95 } 96 //如果缓存中不包含这个key,就将key对应的value设置为默认值null 97 for (Object entry : entriesMissedInCache) { 98 if (!entriesToAddOnCommit.containsKey(entry)) { 99 delegate.putObject(entry, null); 100 } 101 } 102 } 103 104 // 移除缺失的key,就是这个缓存中没有的key都移除掉 105 private void unlockMissedEntries() { 106 for (Object entry : entriesMissedInCache) { 107 try { 108 delegate.removeObject(entry); 109 } catch (Exception e) { 110 log.warn("Unexpected exception while notifiying a rollback to the cache adapter." 111 + "Consider upgrading your cache adapter to the latest version. Cause: " + e); 112 } 113 } 114 } 115 116} 117

2.10 WeakCache

从名字上看跟 SoftCache 有点关系,Soft引用是当内存不够用时候才清理, 而Weak 弱引用则相反, 只要有GC就会回收。 所以他们的类型特性并不是自己实现的,而是依赖于 Reference<T> 类的特性,所以代码就不看了基本和 SoftCache 实现一摸一样。