1. 程式人生 > 程式設計 >快取策略解析

快取策略解析

在看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,-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,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,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和部分淘汰機制進行的快取分析,這裡做的一些處理是有侷限性的,所以要根據不同的情況來進行判斷。也許看的時候雲裡霧裡,不過在結合實踐之後,會有更清晰的認識