缓存策略解析

1,351 阅读20分钟

在看java缓存策略之前先看一下目前java中存在的淘汰机制。 这里主要讲的是LFU,LRU,FIFO:

FIFO(First in First out),先进先出。其实在操作系统的设计理念中很多地方都利用到了先进先出的思想,比如作业调度(先来先服务),为什么这个原则在很多地方都会用到呢?因为这个原则简单、且符合人们的惯性思维,具备公平性,并且实现起来简单,直接使用数据结构中的队列即可实现。

  在FIFO Cache设计中,核心原则就是:如果一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。在FIFO Cache中应该支持以下操作;

   特点:

1)Object get(key):获取保存的数据,如果数据不存在或者已经过期,则返回null。 2)void put(key,value,expireTime):加入缓存,无论此key是否已存在,均作为新key处理(移除旧key);如果空间不足,则移除已过期的key,如果没有,则移除最早加入缓存的key。过期时间未指定,则表示永不自动过期。 3)此题需要注意,我们允许key是有过期时间的,这一点与普通的FIFO有所区别,所以在设计此题时需要注意。(也是面试考察点,此题偏设计而非算法) 普通的FIFO或许大家都能很简单的写出,此处增加了过期时间的特性,所以在设计时需要多考虑。如下示例,为FIFO的简易设计,尚未考虑并发环境场景。 设计思路 1)用普通的hashMap保存缓存数据。 2)我们需要额外的map用来保存key的过期特性,例子中使用了TreeMap,将“剩余存活时间”作为key,利用treemap的排序特性。

public class FIFOCache {  
  
    //按照访问时间排序,保存所有key-value  
    private final Map<String,Value> CACHE = new LinkedHashMap<>();  
  
    //过期数据,只保存有过期时间的key  
    //暂不考虑并发,我们认为同一个时间内没有重复的key,如果改造的话,可以将value换成set  
    private final TreeMap<Long, String> EXPIRED = new TreeMap<>();  
  
    private final int capacity;  
  
    public FIFOCache(int capacity) {  
        this.capacity = capacity;  
    }  
  
    public Object get(String key) {  
        //  
        Value value = CACHE.get(key);  
        if (value == null) {  
            return null;  
        }  
  
        //如果不包含过期时间  
        long expired = value.expired;  
        long now = System.nanoTime();  
        //已过期  
        if (expired > 0 && expired <= now) {  
            CACHE.remove(key);  
            EXPIRED.remove(expired);  
            return null;  
        }  
        return value.value;  
    }  
  
    public void put(String key,Object value) {  
        put(key,value,-1);  
    }  
  
  
    public void put(String key,Object value,int seconds) {  
        //如果容量不足,移除过期数据  
        if (capacity < CACHE.size()) {  
            long now = System.nanoTime();  
            //有过期的,全部移除  
            Iterator<Long> iterator = EXPIRED.keySet().iterator();  
            while (iterator.hasNext()) {  
                long _key = iterator.next();  
                //如果已过期,或者容量仍然溢出,则删除  
                if (_key > now) {  
                    break;  
                }  
                //一次移除所有过期key  
                String _value = EXPIRED.get(_key);  
                CACHE.remove(_value);  
                iterator.remove();  
            }  
        }  
  
        //如果仍然容量不足,则移除最早访问的数据  
        if (capacity < CACHE.size()) {  
            Iterator<String> iterator = CACHE.keySet().iterator();  
            while (iterator.hasNext() && capacity < CACHE.size()) {  
                String _key = iterator.next();  
                Value _value = CACHE.get(_key);  
                long expired = _value.expired;  
                if (expired > 0) {  
                    EXPIRED.remove(expired);  
                }  
                iterator.remove();  
            }  
        }  
  
        //如果此key已存在,移除旧数据  
        Value current = CACHE.remove(key);  
        if (current != null && current.expired > 0) {  
            EXPIRED.remove(current.expired);  
        }  
        //如果指定了过期时间  
        if(seconds > 0) {  
            long expireTime = expiredTime(seconds);  
            EXPIRED.put(expireTime,key);  
            CACHE.put(key,new Value(expireTime,value));  
        } else {  
            CACHE.put(key,new Value(-1,value));  
        }  
  
    }  
  
    private long expiredTime(int expired) {  
        return System.nanoTime() + TimeUnit.SECONDS.toNanos(expired);  
    }  
  
    public void remove(String key) {  
        Value value = CACHE.remove(key);  
        if(value == null) {  
            return;  
        }  
        long expired = value.expired;  
        if (expired > 0) {  
            EXPIRED.remove(expired);  
        }  
    }  
  
  
    class Value {  
        long expired; //过期时间,纳秒  
        Object value;  
        Value(long expired,Object value) {  
            this.expired = expired;  
            this.value = value;  
        }  
    }  
}  

LRU:

least recently used,最近最少使用,是目前最常用的缓存算法和设计方案之一,其移除策略为“当缓存(页)满时,优先移除最近最久未使用的数据”,优点是易于设计和使用,适用场景广泛。算法可以参考leetcode 146 (LRU Cache)。

特点 1)Object get(key):从canche中获取key对应的数据,如果此key已过期,移除此key,并则返回null。 2)void put(key,value,expired):设置k-v,如果容量不足,则根据LRU置换算法移除“最久未被使用的key”,需要注意,根据LRU优先移除已过期的keys,如果没有,则根据LRU移除未过期的key。如果未设定过期时间,则认为永不自动过期。 3)此题,设计关键是过期时间特性,这与常规的LRU有所不同。毕竟“过期时间”特性在cache设计中是必要的。 设计思路 1)LRU的基础算法,需要了解;每次put、get时需要更新key对应的访问时间,我们需要一个数据结构能够保存key最近的访问时间且能够排序。 2)既然包含过期时间特性,那么带有过期时间的key需要额外的数据结构保存。 3)暂时不考虑并发操作;尽量兼顾空间复杂度和时间复杂度。 4)此题仍然偏向于设计题,而非纯粹的算法题。 此题代码与FIFO基本相同,唯一不同点为get()方法,对于LRU而言,get方法需要重设访问时间(即调整所在cache中顺序)

public Object get(String key) {  
    //  
    Value value = CACHE.get(key);  
    if (value == null) {  
        return null;  
    }  
  
    //如果不包含过期时间  
    long expired = value.expired;  
    long now = System.nanoTime();  
    //已过期  
    if (expired > 0 && expired <= now) {  
        CACHE.remove(key);  
        EXPIRED.remove(expired);  
        return null;  
    }  
    //相对于FIFO,增加顺序重置  
    CACHE.remove(key);  
    CACHE.put(key,value);  
    return value.value;  
}  

LFU 最近最不常用,当缓存容量满时,移除访问次数最少的元素,如果访问次数相同的元素有多个,则移除最久访问的那个。设计要求参见leetcode 460( LFU Cache)

public class LFUCache {  
  
    //主要容器,用于保存k-v  
    private Map<String, Object> keyToValue = new HashMap<>();  
  
    //记录每个k被访问的次数  
    private Map<String, Integer> keyToCount = new HashMap<>();  
  
    //访问相同次数的key列表,按照访问次数排序,value为相同访问次数到key列表。  
    private TreeMap<Integer, LinkedHashSet<String>> countToLRUKeys = new TreeMap<>();  
  
    private int capacity;  
  
    public LFUCache(int capacity) {  
        this.capacity = capacity;  
        //初始化,默认访问1次,主要是解决下文  
    }  
  
    public Object get(String key) {  
        if (!keyToValue.containsKey(key)) {  
            return null;  
        }  
  
        touch(key);  
        return keyToValue.get(key);  
    }  
  
    /** 
     * 如果一个key被访问,应该将其访问次数调整。 
     * @param key 
     */  
    private void touch(String key) {  
        int count = keyToCount.get(key);  
        keyToCount.put(key, count + 1);//访问次数增加  
        //从原有访问次数统计列表中移除  
        countToLRUKeys.get(count).remove(key);  
  
        //如果符合最少调用次数到key统计列表为空,则移除此调用次数到统计  
        if (countToLRUKeys.get(count).size() == 0) {  
            countToLRUKeys.remove(count);  
        }  
  
        //然后将此key的统计信息加入到管理列表中  
        LinkedHashSet<String> countKeys = countToLRUKeys.get(count + 1);  
        if (countKeys == null) {  
            countKeys = new LinkedHashSet<>();  
            countToLRUKeys.put(count + 1,countKeys);  
        }  
        countKeys.add(key);  
    }  
  
    public void put(String key, Object value) {  
        if (capacity <= 0) {  
            return;  
        }  
  
        if (keyToValue.containsKey(key)) {  
            keyToValue.put(key, value);  
            touch(key);  
            return;  
        }  
        //容量超额之后,移除访问次数最少的元素  
        if (keyToValue.size() >= capacity) {  
            Map.Entry<Integer,LinkedHashSet<String>> entry = countToLRUKeys.firstEntry();  
            Iterator<String> it = entry.getValue().iterator();  
            String evictKey = it.next();  
            it.remove();  
            if (!it.hasNext()) {  
                countToLRUKeys.remove(entry.getKey());  
            }  
            keyToCount.remove(evictKey);  
            keyToValue.remove(evictKey);  
  
        }  
  
        keyToValue.put(key, value);  
        keyToCount.put(key, 1);  
        LinkedHashSet<String> keys = countToLRUKeys.get(1);  
        if (keys == null) {  
            keys = new LinkedHashSet<>();  
            countToLRUKeys.put(1,keys);  
        }  
        keys.add(key);  
    }  
}  

这里有一道LeetCode的题目大家可以根据这个题目来更好的思考LRU缓存机制

运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 该操作会使得密钥 2 作废
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

关于FIFO,LRU,LFU先介绍这么多,老规矩咱们还是从上面的这几种淘汰规则开始,来进入正题的Guava 源码分析。

Example

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .removalListener(MY_LISTENER)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

适用性

缓存在各种用例中非常有用。例如,当值计算或检索的代价很高时,您应该考虑使用缓存,并且您需要多次在某个输入上使用它的值。 每个Cache都类似于ConcurrentMap,但又不是完全相同。最根本的区别在于:ConcurrentMap在显示删除元素之前,会保留添加到ConcurrentMap中的所有元素。而Cache在通常情况下默认配置为自动移除,以便减少元素占用的内存空间。只有在特定情况下如LoadingCache的时候,会自动缓存内容,即使没有真正的移除元素,Cache仍旧比ConcurrentMap有效率一些

Guava缓存适用于程序中的情况,一般如下: 用适量的内存来提高效率。 对于一些数据需要反复查询。 你的缓存不会存储大量数据比RAM。(Guava缓存应用程序运行一次后的本地缓存,不会讲数据存储在文件或者外部服务器上,如果不是这些需求,可以考虑 Memcached.)

如上面的实例代码所示,Cache使用CacheBuilder建造者模式来获取对象的,对于自定义缓存来说这部分是很有意义的地方。

注意:如果您不需要定制的Cache,ConcurrentHashMap则内存效率更高 - 但是Cache使用任何旧功能集成了大多数功能都非常强大更加不同于ConcurrentMap。

总体

当你问自己关于缓存的第一个问题是:是否有一些合理的默认函数来加载或计算与密钥相关的值?如果是这样,你应该使用CacheLoader。如果没有,或者你需要覆盖默认值,但仍然需要原子“get-if-absent-compute”语义,那么你应该将传递给 Callable来获取调用。可以使用直接插入元素 Cache.put,但首选自动缓存加载,因为它可以更容易地推断所有缓存内容的一致性。

关于CacheLoader

Cache内置附带的CacheLoader是LoadingCache。创建CacheLoader与实现CacheLoader和普通的方法一样简单,实现load(K key)抛出异常。因此,如果你想创建一个LoadingCache可以参考下面的代码示例:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

...
try {
  return graphs.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

规范的查询LoadingCache的方式是使用该方法的get(k)。这将返回已缓存的值,或者使用CachaLoader缓存以原子操作的方式将新值加载到缓存中。西因此CacheLoader可能抛出异常。(如果缓存加载器抛出一个未经建厂的异常,get(k)将抛出UncheckedExecutionException的异常)。您也可以选择使用getUnchecked(K),它包含所有异常 UncheckedExecutionException,但如果底层CacheLoader通常会抛出已检查的异常,由于这种情况可能会出现意料之外的情况 。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .expireAfterAccess(10, TimeUnit.MINUTES)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return createExpensiveGraph(key);
             }
           });

return graphs.getUnchecked(key);

可以使用该方法执行批量查找getAll(Iterable<? extends K>)。默认情况下,getAll将对CacheLoader.load缓存中不存在的每个密钥发出单独的调用。当批量检索比许多单独查找更有效时,您可以覆盖CacheLoader.loadAll以利用它。表现getAll(Iterable)将相应提高。

请注意,您可以编写一个CacheLoader.loadAll实现来加载未特别请求的键的值。例如,如果计算某个组中任何键的值为您提供组中所有键的值,则 loadAll可能同时加载该组的其余部分。

关于回调

所有Guava缓存,无论是否加载,都支持该方法get(K, Callable)。此方法返回与缓存中的键关联的值,或者从指定的位置计算它Callable并将其添加到缓存中。在加载完成之前,不会修改与此高速缓存关联的可观察状态。此方法提供了传统“if cached,return;否则create,cache和return”模式的简单替代。

Cache<Key, Value> cache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .build(); // look Ma, no CacheLoader
...
try {
  // If the key wasn't in the "easy to compute" group, we need to
  // do things the hard way.
  cache.get(key, new Callable<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

缓存插入

可以直接将值插入缓存中cache.put(key, value)。这将覆盖指定键的缓存中的任何先前条目。还可以使用视图ConcurrentMap公开的任何方法对缓存进行更改Cache.asMap()。请注意,asMap视图上的任何方法都不会导致条目自动加载到缓存中。此外,在该视图中的原子操作自动高速缓存加载的范围之外进行操作,所以 Cache.get(K, Callable)应始终优于 Cache.asMap().putIfAbsent在负荷值使用其中或者高速缓存 CacheLoader或Callable。

缓存移除

实际情况肯定不允许我们缓存所有内容,所以就引来了一个问题:何时不值得保留缓存条目?Guava提供三种基本类型的缓存空间的回收:基于规模的缓存空间回收,基于时间的缓存空间回收和基于参考目标的缓存回收

基于规模的缓存空间回收

如果您的缓存不应超过一定的大小,请使用 CacheBuilder.maximumSize(long)。缓存将尝试回收最近或非常常使用的条目。警告:缓存可能会在超出此限制之前逐出条目 - 通常是在缓存大小接近限制时。

或者,如果不同的缓存条目具有不同的“权重” - 例如,如果缓存值具有完全不同的内存占用量 - 您可以指定权重函数CacheBuilder.weigher(Weigher)和最大缓存权重CacheBuilder.maximumWeight(long)。除了与要求相同的警告之外,请注意maximumSize权重是在条目创建时计算的,并且此后是静态的。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumWeight(100000)
       .weigher(new Weigher<Key, Graph>() {
          public int weigh(Key k, Graph g) {
            return g.vertices().size();
          }
        })
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return createExpensiveGraph(key);
             }
           });

定时缓存回收 CacheBuilder 提供了两种定时缓存回收的方法:

expireAfterAccess(long, TimeUnit)只有在读取或写入最后一次访问条目后经过指定的持续时间后才会使条目到期。请注意,条目被驱逐的顺序与基于大小的驱逐的顺序类似。 expireAfterWrite(long, TimeUnit)在创建条目后经过指定的持续时间或最近更换值后,使条目过期。如果缓存数据在一定时间后变得陈旧,则可能需要这样做。 如下所述,在写入期间以及在读取期间偶尔进行定期维护来执行定时到期。

测试定时缓存回收 测试定时缓存回收并不一定是困难的......并且实际上不需要花费两秒钟来测试两秒钟的到期时间。使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存构建器中指定时间源,而不必等待系统时钟。

基于参考目标的缓存回收

Guava允许您设置缓存以允许条目的垃圾收集,使用对键或值的弱引用,以及使用值的软引用。

CacheBuilder.weakKeys()使用弱引用存储密钥。如果没有其他(强或软)引用键,则允许对条目进行垃圾回收。由于垃圾收集仅依赖于身份相等性,因此这会导致整个缓存使用identity(==)相等来比较键,而不是equals()。 CacheBuilder.weakValues()使用弱引用存储值。如果没有对值的其他(强或软)引用,则允许对条目进行垃圾收集。由于垃圾收集仅依赖于身份相等性,因此这会导致整个缓存使用identity(==)相等来比较值,而不是equals()。 CacheBuilder.softValues()在软引用中包装值。为响应内存需求,软件引用的对象以全球最近最少使用的方式进行垃圾收集。由于使用软引用的性能影响,我们通常建议使用更可预测的最大缓存大小。使用softValues()将导致使用identity(==)相等而不是比较值equals()。 标记缓存回收 您可以随时明确地使缓存条目无效,而不是等待条目被回收。这可以做到:

单独使用 Cache.invalidate(key) 批量使用 Cache.invalidateAll(keys) 使用的所有条目 Cache.invalidateAll()

删除监听

您可以为缓存指定删除监听,以便在删除条目时执行某些操作CacheBuilder.removalListener(RemovalListener)。在 RemovalListener被通过了RemovalNotification,它指定了 RemovalCause,key,和value。

CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
  public DatabaseConnection load(Key key) throws Exception {
    return openConnection(key);
  }
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
  public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
    DatabaseConnection conn = removal.getValue();
    conn.close(); // tear down properly
  }
};

return CacheBuilder.newBuilder()
  .expireAfterWrite(2, TimeUnit.MINUTES)
  .removalListener(removalListener)
  .build(loader);

警告:默认情况下,删除监听操作是同步执行的,并且由于缓存维护通常在正常缓存操作期间执行,因此耗费资源的删除监听会降低正常的缓存功能!如果你有一个耗费资源的删除监听,用于 RemovalListeners.asynchronous(RemovalListener, Executor)装饰一个 RemovalListener异步操作。

什么时候清理会发生? 内置缓存CacheBuilder也没有一个值到期后进行清理和回收值“自动”或瞬间,或诸如此类的事。相反,它在写入操作期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。

原因如下:如果我们想要Cache连续执行维护,我们需要创建一个线程,它的操作将与共享锁的用户操作竞争。此外,某些环境会限制线程的创建,这会使CacheBuilder该环境无法使用。

相反,我们把选择放在你手中。如果您的缓存是高吞吐量,那么您不必担心执行缓存维护以清理过期的条目等。如果您的缓存很少写入并且您不希望清除阻止缓存读取,您可能希望创建自己的维护线程,该线程Cache.cleanUp()定期调用。

如果要为很少写入的高速缓存安排常规高速缓存维护,只需使用安排维护ScheduledExecutorService。

刷新

刷新与回收并不完全相同。如上所述 LoadingCache.refresh(K),刷新密钥会加载密钥的新值,可能是异步的。在刷新密钥时仍会返回旧值(如果有的话),与回收相反,回收会强制检索等到重新加载该值。

如果在刷新时抛出异常,则保留旧值,并记录并吞下异常。

A CacheLoader可以指定通过覆盖在刷新时使用的智能行为 CacheLoader.reload(K, V),这允许您在计算新值时使用旧值。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(1000)
       .refreshAfterWrite(1, TimeUnit.MINUTES)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) { // no checked exception
               return getGraphFromDatabase(key);
             }

             public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
               if (neverNeedsRefresh(key)) {
                 return Futures.immediateFuture(prevGraph);
               } else {
                 // asynchronous!
                 ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
                   public Graph call() {
                     return getGraphFromDatabase(key);
                   }
                 });
                 executor.execute(task);
                 return task;
               }
             }
           });

可以使用自动定时刷新添加到缓存 CacheBuilder.refreshAfterWrite(long, TimeUnit)。与此相反 expireAfterWrite,refreshAfterWrite将使键在指定的持续时间后符合刷新条件,但只有在查询条目时才会实际刷新。(如果CacheLoader.reload实现是异步的,那么该查询将不会被刷新放缓。)因此,例如,您可以同时指定refreshAfterWrite,并expireAfterWrite在同一高速缓存,使一个条目的过期计时器不能盲目复位时条目有资格进行刷新,因此如果条目在符合刷新条件后未被查询,则允许其过期。

特征

统计

通过使用CacheBuilder.recordStats(),您可以打开Guava缓存的统计信息收集。该Cache.stats()方法返回一个CacheStats对象,该对象提供诸如的统计信息

hitRate(),返回命中率与请求的比率 averageLoadPenalty(),加载新值所花费的平均时间,以纳秒为单位 evictionCount(),缓存回收的数量 还有更多的统计数据。这些统计信息在缓存调整中至关重要,我们建议在性能关键型应用程序中密切关注这些统计信息。

asMap 您可以查看任何Cache一个ConcurrentMap使用它的asMap观点,但如何asMap查看与交互Cache需要一些解释。

cache.asMap()包含当前在缓存中加载的所有条目。因此,例如,cache.asMap().keySet()包含所有当前加载的密钥。 asMap().get(key)本质上等同于cache.getIfPresent(key),并且永远不会导致值被加载。这与Map 合同一致。 访问时间由所有缓存读写操作(包括Cache.asMap().get(Object)和Cache.asMap().put(K, V))重置,但不是由 containsKey(Object)集合视图上的操作重置,也不是由集合视图上的操作 重置 Cache.asMap()。因此,例如,迭代 cache.asMap().entrySet()不会重置您检索的条目的访问时间。

中断

加载方法(如get)从不抛出InterruptedException。我们本来可以设计这些方法来支持InterruptedException,但我们的支持不完整,迫使所有用户付出代价,但只有部分用户受益。有关详情,请继续阅读。

get请求未缓存值的调用分为两大类:加载值的那些和等待另一个线程正在进行的加载的类。这两者在支持中断方面的能力不同。简单的情况是等待另一个线程正在进行的加载:在这里我们可以输入一个可中断的等待。困难的情况是我们自己加载价值。在这里,我们受到用户提供的支配CacheLoader。如果它恰好支持中断,我们可以支持中断; 如果没有,我们不能。

那么为什么不提供支持中断CacheLoader呢?从某种意义上说,我们(但见下文):如果CacheLoader抛出 InterruptedException,所有get对密钥的调用都会立即返回(就像任何其他异常一样)。另外,get将恢复加载线程中的中断位。令人惊讶的是,它InterruptedException被包裹在一个ExecutionException。

原则上,我们可以为您解开此例外。但是,这会强制所有 LoadingCache用户处理InterruptedException,即使大多数CacheLoader实现从不抛出它。当您考虑到所有非加载线程的等待仍然可能被中断时,这仍然是值得的。但是许多缓存仅在单个线程中使用。他们的用户仍然必须抓住不可能的InterruptedException。甚至谁在线程之间共享其缓存的用户将能够打断他们get只调用有时基于哪个线程碰巧先提出请求。

我们在此决策中的指导原则是缓存的行为就好像所有值都在调用线程中加载一样。这个原则可以很容易地将缓存引入到以前在每次调用中重新计算其值的代码中。如果旧代码不可中断,那么新代码也可能不行。

我说我们支持“在某种意义上”的中断。还有另一种感觉,我们不这样做,造成LoadingCache漏洞抽象。如果加载线程被中断,我们会像任何其他异常一样对待它。在许多情况下这很好,但是当多个get调用等待值时,这不是正确的事情。虽然正在计算该值的操作被中断,但是其他需要该值的操作可能没有。然而,所有这些呼叫者都接收到InterruptedException(包裹在一起 ExecutionException),即使负载没有像“中止”那样“失败”。正确的行为是其余一个线程重试负载。我们为此提交了一个错误。但是,修复可能存在风险。我们可以在建议中投入额外的精力,而不是解决问题,AsyncLoadingCache这会使Future对象返回 正确的中断行为。

以上就是依据Guava和部分淘汰机制进行的缓存分析,这里做的一些处理是有局限性的,所以要根据不同的情况来进行判断。也许看的时候云里雾里,不过在结合实践之后,会有更清晰的认识