精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

深入解析 Guava Cache- 從基本用法、回收策略、刷新策略到實現(xiàn)原理

開發(fā) 前端
Guava Cache 支持 segment 粒度上支持了 LRU 機制, 體現(xiàn)在 Segment 上就是 writeQueue 和 accessQueue。 隊列中的元素按照訪問或者寫時間排序,新的元素會被添加到隊列尾部。如果,在隊列中已經(jīng)存在了該元素,則會先delete掉,然后再尾部添加該節(jié)點。

Guava Cache 是非常強大的本地緩存工具,提供了非常簡單 API 供開發(fā)者使用。

這篇文章,我們將詳細介紹 Guava Cache 的基本用法回收策略刷新策略實現(xiàn)原理

圖片圖片

1.基本用法

1.1 依賴配置

<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>31.0.1-jre</version>
</dependency>

1.2 創(chuàng)建緩存

Guava Cache 提供了基于 Builder 構(gòu)建者模式的構(gòu)造器,用戶只需要根據(jù)需求設(shè)置好各種參數(shù)即可使用。

(1)手工創(chuàng)建緩存對象

@Test
public void testHandCache() {
      // 測試手工測試
      Cache<String, String> cache = CacheBuilder.newBuilder().
              // 最大容量為20(基于容量進行回收)
                      maximumSize(20)
              // 配置寫入后多久未更新,緩存會過期
              .expireAfterWrite(10, TimeUnit.SECONDS).build();
      cache.put("hello", "value_HELLO");
      assertEquals("value_HELLO", cache.getIfPresent("hello"));
      Thread.sleep(10000);
      // 過期后重新獲取 
      assertNull(cache.getIfPresent("hello"));
}

我們可以創(chuàng)建一個緩存對象 Cache ,通過 CacheBuilder 構(gòu)造器,配置相關(guān)參數(shù)(最大容量 20 個條目、緩存過期時間 10 秒),最后調(diào)用構(gòu)建方法。

(2)創(chuàng)建緩存加載器

CacheLoader 可以理解為一個固定的加載器,在創(chuàng)建 Cache 對象時指定,然后簡單地重寫 V load(K key) throws Exception 方法,就可以達到當檢索不存在的時候,會自動的加載數(shù)據(jù)。

@Test
public void testLoadingCache() throws InterruptedException, ExecutionException {
      CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
          //自動寫緩存數(shù)據(jù)的方法
          @Override
          public String load(String key) {
              System.out.println("加載 key:" + key);
              return"value_" + key.toUpperCase();
          }
          @Override
          //重新刷新緩存
          public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
              returnsuper.reload(key, oldValue);
          }
      };

      LoadingCache<String, String> cache =
              CacheBuilder.newBuilder()
                      // 最大容量為100(基于容量進行回收)
                      .maximumSize(20)
                      // 配置寫入后多久未更新,緩存會過期
                      .expireAfterWrite(10, TimeUnit.SECONDS)
                      //配置寫入后多久刷新緩存
                      .refreshAfterWrite(1, TimeUnit.SECONDS).build(cacheLoader);
        assertEquals(0, cache.size());
        assertEquals("value_HELLO", cache.getUnchecked("hello"));
        assertEquals(1, cache.size());

     // 通過 Callable 獲取數(shù)據(jù)
       String key = "mykey";
       String value = cache.get(key, new Callable<String>() {
          @Override
           public String call() throws Exception {
               return"call_" + key;
          }
        });
       System.out.println("call value:" + value);
}

和手工創(chuàng)建緩存對象不同,我們首先創(chuàng)建緩存加載器對象,并重寫 load 方法,然后通過緩存構(gòu)造器創(chuàng)建 LoadingCache 對象 ,該對象支持寫入后刷新方法。

同時 LoadingCache 對象支持 Callable 模式,也就是調(diào)用 get 方法時,可以傳入 Callable 對象。這樣可以在使用緩存時,更加靈活。

2.回收策略

Guava Cache 提供了三種基本的緩存回收方式:

  • 基于容量回收策略
  • 基于時間的回收策略
  • 基于引用回收策略

2.1 基于容量回收策略

基于容量的回收策略可以分為兩種:基于大小基于權(quán)重

基于大小:我們可以使用 maximumSize 方法設(shè)置最大緩存項數(shù)量,當緩存項數(shù)量達到設(shè)定的最大值時,舊的緩存項將會被移除。

Cache<Object, Object> cache = CacheBuilder.newBuilder()
    .maximumSize(100)
    .build();

基于權(quán)重:如果不同的緩存值,需要占據(jù)不同的內(nèi)存空間,也就是不同的緩存項有不同的“權(quán)重”(weights)。

我們可以使用 CacheBuilder.weigher(Weigher) 指定一個權(quán)重函數(shù),并且用 maximumWeight(long) 指定最大總重。

Cache<Object, Object> cache = CacheBuilder.newBuilder()
    .maximumWeight(1000)
    .weigher(new Weigher<Object, Object>() {
        public int weigh(Object key, Object value) {
            // 定義權(quán)重計算方法
            return value.size();
        }
    }).build();

2.2 基于時間的回收策略

我們可以使用 expireAfterAccess 和 expireAfterWrite 方法設(shè)置緩存項的最大存活時間。

  • expireAfterAccess 表示緩存項在給定時間內(nèi)沒有被讀/寫訪問會過期。
  • expireAfterWrite 表示緩存項在被創(chuàng)建或最后一次更新后的指定時間內(nèi)會過期。
Cache<Object, Object> cache = CacheBuilder.newBuilder()
    // 10分鐘沒有訪問后會被回收,或者重新加載
    .expireAfterAccess(10, TimeUnit.MINUTES)
    // 5分鐘沒有更新,緩存會被回收,或者重新加載
    // .expireAfterWrite(5,TimeUnit.MINUTES)
.build();

2.3 基于引用回收策略

Guava Cache 提供了以下三個方法來配置基于引用的回收策略:

  • weakKeys() 方法:
    通過調(diào)用 weakKeys() 方法,可以使緩存中的鍵使用弱引用。這意味著如果某個鍵沒有其他強引用指向它,那么該鍵可能會被垃圾回收,并且相應(yīng)的緩存項也會被移除。
Cache<Object, Object> cache = CacheBuilder.newBuilder()
       .weakKeys()
       .build();
  • weakValues() 方法:
    通過調(diào)用 weakValues() 方法,可以使緩存中的值使用弱引用。這樣,如果某個值沒有其他強引用指向它,那么該值可能會被垃圾回收,相應(yīng)的緩存項也會被移除。
Cache<Object, Object> cache = CacheBuilder.newBuilder()
       .weakValues()
       .build();
  • softValues() 方法:
    通過調(diào)用 softValues() 方法,可以使緩存中的值使用軟引用。軟引用相對于弱引用,更傾向于在內(nèi)存不足時被垃圾回收。如果某個值沒有其他強引用指向它,且內(nèi)存不足時,該值可能會被垃圾回收,相應(yīng)的緩存項也會被移除。
Cache<Object, Object> cache = CacheBuilder.newBuilder()
       .softValues()
       .build();

一般來講,我們在生產(chǎn)環(huán)境使用的是(基于容量回收策略 + 基于時間的回收策略)兩者配合來使用。

當然 ,我們同樣可以使用手工回收的方式。

Cache<String,String> cache = CacheBuilder.newBuilder().build();
Object value = new Object();
cache.put("key1","value1");
cache.put("key2","value2");
cache.put("key3","value3");

//1.清除指定的key
cache.invalidate("key1");

//2.批量清除list中全部key對應(yīng)的記錄
List<String> list = new ArrayList<String>();
list.add("key1");
list.add("key2");
cache.invalidateAll(list);

3.刷新策略

3.1 手工刷新

我們可以強制緩存加載器重新加載鍵的新值,調(diào)用 LoadingCache 對象的刷新方法。

String value = loadingCache.get("key");
loadingCache.refresh("key");

3.2 自動刷新

Guava Cache 提供了刷新(refresh)機制,可以通過 refreshAfterWrite 方法來設(shè)置刷新時間,當緩存項過期的同時可以重新加載新值。

Cache<String, String> cache = CacheBuilder.newBuilder()
    .refreshAfterWrite(5, TimeUnit.MINUTES)
     // 設(shè)置并發(fā)級別為3,并發(fā)級別是指可以同時寫緩存的線程數(shù)
    .concurrencyLevel(3)
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            // 異步加載新值的邏輯
            return fetchDataFromDataSource(key);
        }
    });
// 在獲取緩存值時,如果緩存項過期,將返回舊值 
String value = cache.get("exampleKey");

配置刷新方法 refreshAfterWrite,當大量線程同時訪問緩存項,緩存已過期時,更新線程調(diào)用 load 方法更新該緩存,其他請求線程并不需要等待,框架直接返回該緩存項的舊值。

因為更新線程同時也是請求線程,所以在上面的示例代碼里面,刷新緩存是個同步操作,可不可以異步的加載緩存呢 ?

我們有兩種方式:異步加載緩存的原理是重寫 reload 方法

@Test
public void testAnsynRefreshMethod1() throws InterruptedException, ExecutionException {
      ExecutorService executorService = Executors.newFixedThreadPool(5);
      CacheLoader<String, String> cacheLoader = new CacheLoader<String, String>() {
          //自動寫緩存數(shù)據(jù)的方法
          @Override
          public String load(String key) {
              System.out.println(Thread.currentThread().getName() + " 加載 key:" + key);
              // 從數(shù)據(jù)庫加載數(shù)據(jù)
              return"value_" + key.toUpperCase();
          }

          @Override
          //異步刷新緩存
          public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
              ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
                  System.out.println(Thread.currentThread().getName() + " 異步加載 key:" + key + " oldValue:" + oldValue);
                  Thread.sleep(1000);
                  return load(key);
              });
              executorService.submit(futureTask);
              return futureTask;
          }
      };

      LoadingCache<String, String> cache = CacheBuilder.newBuilder()
              // 最大容量為20(基于容量進行回收)
              .maximumSize(20)
              //配置寫入后多久刷新緩存
              .refreshAfterWrite(2, TimeUnit.SECONDS).build(cacheLoader);

       String key = "hello";
       // 第一次加載
       String value = cache.get(key);
       System.out.println(value);
       Thread.sleep(3000);
      for (int i = 0; i < 10; i++) {
          executorService.execute(new Runnable() {
              @Override
              public void run() {
                  try {
                      String value2 = cache.get(key);
                      System.out.println(Thread.currentThread().getName() + value2);
                      // 第二次加載
                  } catch (Exception e) {
                       e.printStackTrace();
                  }
              }
          });
      }
      Thread.sleep(20000);
}

或者使用更優(yōu)雅的使用方式:

ExecutorService executorService = Executors.newFixedThreadPool(5);
CacheLoader<String, String> cacheLoader = CacheLoader.asyncReloading(
           new CacheLoader<String, String>() {
                  //自動寫緩存數(shù)據(jù)的方法
                  @Override
                  public String load(String key) {
                      System.out.println(Thread.currentThread().getName() + " 加載 key:" + key);
                      // 從數(shù)據(jù)庫加載數(shù)據(jù)
                      return "value_" + key.toUpperCase();
                  }
            } , executorService);

自動刷新的缺點是:當緩存項到了指定過期時間,不管是同步刷新還是異步刷新,絕大部分請求線程都會返回舊的數(shù)據(jù)值,緩存值會有一定的延遲效果。

所以一般場景下,使用efreshAfterWrite和 expireAfterWrite配合使用 。

比如說控制緩存每1秒進行刷新,如果超過 2s 沒有訪問,那么則讓緩存失效,訪問時不會得到舊值,而是必須得待新值加載。

4.實現(xiàn)原理

Guava Cache 的數(shù)據(jù)結(jié)構(gòu)跟 JDK1.7 的 ConcurrentHashMap 類似,如下圖所示:

圖片圖片

4.1 構(gòu)造函數(shù)

public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
   checkWeightWithWeigher();
   return new LocalCache.LocalLoadingCache<>(this, loader);
}

通過構(gòu)造器 CacheBuilder 的構(gòu)建方法創(chuàng)建本地緩存類 LocalCache 的靜態(tài)包裝類 LocalLoadingCache對象。

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
   // ..... 省略代碼 
   staticclass LocalLoadingCache<K, V> extends LocalManualCache<K, V>
      implements LoadingCache<K, V> {
    
    LocalLoadingCache(
        CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
      super(new LocalCache<K, V>(builder, checkNotNull(loader)));
    }
    // LoadingCache methods
    @Override
    public V get(K key) throws ExecutionException {
      return localCache.getOrLoad(key);
    }
    @Override
    public V getUnchecked(K key) {
      try {
        return get(key);
      } catch (ExecutionException e) {
        thrownew UncheckedExecutionException(e.getCause());
      }
    }
    @Override
    public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException {
      return localCache.getAll(keys);
    }
    @Override
    public void refresh(K key) {
      localCache.refresh(key);
    }
   // ..... 省略代碼 
  }
}

LocalLoadingCache 類對外暴露了若干方法,它的底層依然是 LocalCache 對象來執(zhí)行相關(guān)緩存操作,LocalCache 本質(zhì)上就是一個 Map 。

4.2 初始化緩存

LocalCache(
      CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
    concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
    // key的強度,即引用類型的強弱
    keyStrength = builder.getKeyStrength();
    // value的強度,即引用類型的強弱
    valueStrength = builder.getValueStrength();
    // key的比較策略,跟key的引用類型有關(guān)
    keyEquivalence = builder.getKeyEquivalence();
    // value的比較策略,跟value的引用類型有關(guān)
    valueEquivalence = builder.getValueEquivalence();

    maxWeight = builder.getMaximumWeight();
    weigher = builder.getWeigher();
    //訪問后的過期時間,設(shè)置了expireAfterAccess參數(shù)
    expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
     //寫入后的過期時間,設(shè)置了expireAfterWrite參數(shù)
    expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
    refreshNanos = builder.getRefreshNanos();

    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
    if (evictsBySize() && !customWeigher()) {
      initialCapacity = (int) Math.min(initialCapacity, maxWeight);
    }
    // Find the lowest power-of-two segmentCount that exceeds concurrencyLevel, unless
    // maximumSize/Weight is specified in which case ensure that each segment gets at least 10
    // entries. The special casing for size-based eviction is only necessary because that eviction
    // happens per segment instead of globally, so too many segments compared to the maximum size
    // will result in random eviction behavior.
    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      ++segmentShift;
      segmentCount <<= 1;
    }
    this.segmentShift = 32 - segmentShift;
    segmentMask = segmentCount - 1;

    this.segments = newSegmentArray(segmentCount);
    
    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }
    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }
    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }
}

LocalCache 維護一個 Segment 數(shù)組,數(shù)組大小滿足如下條件:

  1. 數(shù)組大小是 2 的冪次 ,并且小于并發(fā)度 concurrencyLevel ;
  2. 若指定了容量大小,數(shù)組大小乘以 20 要大于緩存權(quán)重 maxWeight (假如設(shè)置容量大小最大值為40,那么 maxWeight 為 40 )。

接下來,我們看看 Segment 類的核心屬性 :

static class Segment<K, V> extends ReentrantLock {
    // 存活的元素大小
    volatileint count;
    // 存活的元素權(quán)重
    long totalWeight;
    //修改、更新的數(shù)量,用來做弱一致性
    int modCount;
    //擴容用
    int threshold;
    //存放Entry的數(shù)組,用來存放Entry,使用AtomicReferenceArray是因為要用CAS來保證原子性
    volatile@Nullable AtomicReferenceArray<ReferenceEntry<K, V>> table;
     //如果key是弱引用的話,那么被 GC 回收后,就會放到ReferenceQueue,要根據(jù)這個queue做一些清理工作
    final@Nullable ReferenceQueue<K> keyReferenceQueue;
    //如果value是弱引用的話,那么被 GC 回收后,就會放到ReferenceQueue,要根據(jù)這個queue做一些清理工作
    final@Nullable ReferenceQueue<V> valueReferenceQueue;
    //記錄哪些entry被訪問,用于accessQueue的更新。
    final Queue<ReferenceEntry<K, V>> recencyQueue;
    // 讀取次數(shù)計數(shù)器
    final AtomicInteger readCount = new AtomicInteger();
    // 如果一個元素新寫入,則會記到這個隊列的尾部,用來做expire
    @GuardedBy("this")
    final Queue<ReferenceEntry<K, V>> writeQueue;
    //讀、寫都會放到這個隊列,用來進行LRU替換算法
    @GuardedBy("this")
    final Queue<ReferenceEntry<K, V>> accessQueue;
}

ReferenceEntry 有幾種引用類型 :

圖片圖片

下圖展示了 StringEntry 核心屬性 :

圖片圖片

每種 Entry 對象都有 Next 屬性 ,指向下一個 Entry 。對象值 valueReference 默認是一個占位符 unSet ,表示沒有被設(shè)置過值。

4.3 查詢流程

進入 LoadingCache 的 get(key) 方法 , 如下代碼所示:

// 1.調(diào)用LoadingCache的getOrLoad 
V getOrLoad(K key) throws ExecutionException {
    return get(key, defaultLoader);
}
// 2.計算 key 的哈希值,并判斷位于哪一個段 Segment,最后通過查詢
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key));
    return segmentFor(hash).get(key, hash, loader);
}

(1)計算 key 對應(yīng)的哈希值

int hash(@Nullable Object key) {
    int h = keyEquivalence.hash(key);
    return rehash(h);
}

(2)定位分段 Segment

Segment<K, V> segmentFor(int hash) {
   // segmentMask =  segmentCount - 1
   return segments[(hash >>> segmentShift) & segmentMask];
}

第二步驟,和 ConcurrentHashMap 類似,通過哈希值計算數(shù)據(jù)存儲在哪一個分段 Segment 。

(3)從定位的分段查詢出對象

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      // 判斷 key、loader 是否為空 
      checkNotNull(key);
      checkNotNull(loader);
      try {
        if (count != 0) { // read-volatile
          // don't call getLiveEntry, which would ignore loading values
          // 根據(jù)hash定位到 table 的第一個 Entry
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            // 獲取當前時間
            long now = map.ticker.read();
            // 獲取當前存活的 Value 
            V value = getLiveValue(e, now);
            if (value != null) {
              //記錄被訪問過
              recordRead(e, now);
              //記錄命中率
              statsCounter.recordHits(1);
              //判斷是否需要刷新,如果需要刷新,那么會去異步刷新,且返回舊值。
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            //如果 Entry 過期了且數(shù)據(jù)還在加載中,則等待直到加載完成。
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }
        // at this point e is either null or expired;
        // 走到這一步表示: 之前沒有寫入過數(shù)據(jù) || 數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中。
        return lockedGetOrLoad(key, hash, loader);
      } catch (ExecutionException ee) {
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
          thrownew ExecutionError((Error) cause);
        } elseif (cause instanceof RuntimeException) {
          thrownew UncheckedExecutionException(cause);
        }
        throw ee;
      } finally {
        postReadCleanup();
      }
 }
A 定位第一個Entry
ReferenceEntry<K, V> getEntry(Object key, int hash) {
    for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
      // 判斷哈希值
      if (e.getHash() != hash) {
        continue;
      }
      // 判斷key
      K entryKey = e.getKey();
      if (entryKey == null) {
        tryDrainReferenceQueues();
        continue;
      }
      if (map.keyEquivalence.equivalent(key, entryKey)) {
        return e;
      }
    }
    returnnull;
}
B 從第一個 Entry 獲取存活的值
V getLiveValue(ReferenceEntry<K, V> entry, long now) {
     if (entry.getKey() == null) {
        tryDrainReferenceQueues();
        return null;
     }
     V value = entry.getValueReference().get();
     if (value == null) {
       tryDrainReferenceQueues();
       return null;
     }
     if (map.isExpired(entry, now)) {
       tryExpireEntries(now);
       return null;
     }
     return value;
}

boolean isExpired(ReferenceEntry<K, V> entry, long now) {
    checkNotNull(entry);
    // 如果配置了 expireAfterAccess ,比較當前時間和 Entry 的 accessTime 比較
    if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {
      returntrue;
    }
    // 如果配置了 expireAfterWrite ,比較當前時間和 Entry 的 writeTime 比較
    if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {
      returntrue;
    }
    returnfalse;
}

假如 Entry 的 key 為空,或者 vlaue 為空,或者過期了,則返回空 。

C 調(diào)度刷新 scheduleRefresh
V scheduleRefresh(
        ReferenceEntry<K, V> entry,
        K key,
        int hash,
        V oldValue,
        long now,
        CacheLoader<? super K, V> loader) {
       //1、是否配置了 refreshAfterWrite
       //2、用 writeTime 判斷是否達到刷新的時間
       //3、是否在加載中,如果是則沒必要再進行刷新
      if (map.refreshes()
          && (now - entry.getWriteTime() > map.refreshNanos)
          && !entry.getValueReference().isLoading()) {
          V newValue = refresh(key, hash, loader, true);
          if (newValue != null) {
              return newValue;
          }
      }
     return oldValue;
}

調(diào)度刷新方法會判斷三個條件 :

  • 配置了刷新時間 refreshAfterWrite
  • 當前時間減去 Entry 的寫入時間大于刷新時間
  • 當前 Entry 未處于加載中

當滿足了三個條件之后,調(diào)用 refresh 方法,當異步加載成功后,返回新值。

V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
     //插入一個 LoadingValueReference ,實質(zhì)是把對應(yīng)Entry的ValueReference替換為新建的LoadingValueReference
     final LoadingValueReference<K, V> loadingValueReference =
         insertLoadingValueReference(key, hash, checkTime);
     if (loadingValueReference == null) {
       returnnull;
     }
     // 調(diào)用異步加載方法loadAsync
     ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
     if (result.isDone()) {
       try {
         return Uninterruptibles.getUninterruptibly(result);
       } catch (Throwable t) {
         // don't let refresh exceptions propagate; error was already logged
       }
     }
     returnnull;
}

首先將 Entry 對象的 ValueReference 包裝為新建的 LoadingValueReference , 表明當前對象正在加載中。

LoadingValueReference<K, V> insertLoadingValueReference(
        final K key, final int hash, boolean checkTime) {
      ReferenceEntry<K, V> e = null;
      lock();
      try {
        long now = map.ticker.read();
        preWriteCleanup(now);
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);
        // Look for an existing entry.
        for (e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            // We found an existing entry.
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()
                || (checkTime && (now - e.getWriteTime() < map.refreshNanos))) {
              // refresh is a no-op if loading is pending
              // if checkTime, we want to check *after* acquiring the lock if refresh still needs
              // to be scheduled
              returnnull;
            }
            // continue returning old value while loading
            ++modCount;
            LoadingValueReference<K, V> loadingValueReference =
                new LoadingValueReference<>(valueReference);
            e.setValueReference(loadingValueReference);
            return loadingValueReference;
          }
        }
        ++modCount;
        LoadingValueReference<K, V> loadingValueReference = new LoadingValueReference<>();
        e = newEntry(key, hash, first);
        e.setValueReference(loadingValueReference);
        table.set(index, e);
        return loadingValueReference;
      } finally {
        unlock();
        postWriteCleanup();
      }
}

接下來,分析異步加載loadAsync方法:

ListenableFuture<V> loadAsync(
        final K key,
        final int hash,
        final LoadingValueReference<K, V> loadingValueReference,
        CacheLoader<? super K, V> loader) {
      final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
      loadingFuture.addListener(
          new Runnable() {
            @Override
            public void run() {
              try {
                getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
              } catch (Throwable t) {
                logger.log(Level.WARNING, "Exception thrown during refresh", t);
                loadingValueReference.setException(t);
              }
            }
          },
          directExecutor());
      return loadingFuture;
}

public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) {
      try {
        // 記錄耗時時間 
        stopwatch.start();
        V previousValue = oldValue.get();
        if (previousValue == null) {
          V newValue = loader.load(key);
          return set(newValue) ? futureValue : Futures.immediateFuture(newValue);
        }
        ListenableFuture<V> newValue = loader.reload(key, previousValue);
        if (newValue == null) {
          return Futures.immediateFuture(null);
        }
        // To avoid a race, make sure the refreshed value is set into loadingValueReference
        // *before* returning newValue from the cache query.
        return transform(
            newValue,
            new com.google.common.base.Function<V, V>() {
              @Override
              public V apply(V newValue) {
                LoadingValueReference.this.set(newValue);
                return newValue;
              }
            },
            directExecutor());
      } catch (Throwable t) {
        ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t);
        if (t instanceof InterruptedException) {
          Thread.currentThread().interrupt();
        }
        return result;
      }
 }

loadAsync 方法流程:

  • 調(diào)用 loadingValueReference 對象的 loadFuture 方法,假如舊數(shù)據(jù)為空值,則同步調(diào)用加載器 loader 的 load 方法 ,并返回包裝了新值的 Future 。
  • 假如舊數(shù)據(jù)不為空值,則調(diào)用加載器 loader 的 reload 方法(此處可以重新實現(xiàn)為異步的方式),經(jīng)過轉(zhuǎn)換操作返回包裝了新值的 Future 。
  • 將新的值存儲在 Entry 對象里。
D 查詢/加載 lockedGetOrLoad

如果之前沒有寫入過數(shù)據(jù) 、 數(shù)據(jù)已經(jīng)過期、 數(shù)據(jù)不是在加載中,則會調(diào)用lockedGetOrLoad方法。

V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
    ReferenceEntry<K, V> e;
    ValueReference<K, V> valueReference = null;
    LoadingValueReference<K, V> loadingValueReference = null;
    //用來判斷是否需要創(chuàng)建一個新的Entry
    boolean createNewEntry = true;
    //segment上鎖
    lock();
    try {
      // re-read ticker once inside the lock
      long now = map.ticker.read();
      //做一些清理工作
      preWriteCleanup(now);

      int newCount = this.count - 1;
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      int index = hash & (table.length() - 1);
      ReferenceEntry<K, V> first = table.get(index);

      //通過key定位entry
      for (e = first; e != null; e = e.getNext()) {
        K entryKey = e.getKey();
        if (e.getHash() == hash
            && entryKey != null
            && map.keyEquivalence.equivalent(key, entryKey)) {
          //找到entry
          valueReference = e.getValueReference();
          //如果value在加載中則不需要重復(fù)創(chuàng)建entry
          if (valueReference.isLoading()) {
            createNewEntry = false;
          } else {
            V value = valueReference.get();
            //value為null說明已經(jīng)過期且被清理掉了
            if (value == null) {
              //寫通知queue
              enqueueNotification(
                  entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
            //過期但還沒被清理
            } elseif (map.isExpired(e, now)) {
              //寫通知queue
              // This is a duplicate check, as preWriteCleanup already purged expired
              // entries, but let's accomodate an incorrect expiration queue.
              enqueueNotification(
                  entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
            } else {
              recordLockedRead(e, now);
              statsCounter.recordHits(1);
              //其他情況則直接返回value
              //來到這步,是不是覺得有點奇怪,我們分析一下: 
              //進入lockedGetOrLoad方法的條件是數(shù)據(jù)已經(jīng)過期 || 數(shù)據(jù)不是在加載中,但是在lock之前都有可能發(fā)生并發(fā),進而改變entry的狀態(tài),所以在上面中再次判斷了isLoading和isExpired。所以來到這步說明,原來數(shù)據(jù)是過期的且在加載中,lock的前一刻加載完成了,到了這步就有值了。
              return value;
            }
            writeQueue.remove(e);
            accessQueue.remove(e);
            this.count = newCount; // write-volatile
          }
          break;
        }
      }
      //創(chuàng)建一個Entry,且set一個新的 LoadingValueReference。
      if (createNewEntry) {
        loadingValueReference = new LoadingValueReference<>();

        if (e == null) {
          e = newEntry(key, hash, first);
          e.setValueReference(loadingValueReference);
          table.set(index, e);
        } else {
          e.setValueReference(loadingValueReference);
        }
      }
    } finally {
      unlock();
      postWriteCleanup();
    }
    //同步加載數(shù)據(jù)
    if (createNewEntry) {
      try {
        synchronized (e) {
          return loadSync(key, hash, loadingValueReference, loader);
        }
      } finally {
        statsCounter.recordMisses(1);
      }
    } else {
      // The entry already exists. Wait for loading.
      return waitForLoadingValue(e, key, valueReference);
    }
}

5.總結(jié)

通過解析 Guava Cache 的實現(xiàn)原理,我們發(fā)現(xiàn) Guava LocalCache 與 ConcurrentHashMap 有以下不同:

  • ConcurrentHashMap ”分段控制并發(fā)“是隱式的(實現(xiàn)中沒有Segment對象),而 LocalCache 是顯式的。
    在 JDK 1.8 之后,ConcurrentHashMap 采用synchronized + CAS 實現(xiàn):當 put 的元素在哈希桶數(shù)組中不存在時,直接 CAS 進行寫操作;在發(fā)生哈希沖突的情況下使用 synchronized 鎖定頭節(jié)點。其實是比分段鎖更細粒度的鎖實現(xiàn),只在特定場景下鎖定其中一個哈希桶,降低鎖的影響范圍。
  • Guava Cache 使用 ReferenceEntry 來封裝鍵值對,并且對于值來說,還額外實現(xiàn)了 ValueReference 引用對象來封裝對應(yīng) Value 對象。
  • Guava Cache 支持過期 + 自動 loader 機制,這也使得其加鎖方式與 ConcurrentHashMap 不同。
  • Guava Cache 支持 segment 粒度上支持了 LRU 機制, 體現(xiàn)在 Segment 上就是 writeQueue 和 accessQueue。
    隊列中的元素按照訪問或者寫時間排序,新的元素會被添加到隊列尾部。如果,在隊列中已經(jīng)存在了該元素,則會先delete掉,然后再尾部添加該節(jié)點。
責任編輯:武曉燕 來源: 勇哥Java實戰(zhàn)
相關(guān)推薦

2025-10-29 01:22:00

2024-08-29 08:28:17

2017-06-29 09:15:36

推薦算法策略

2009-12-11 11:08:31

靜態(tài)路由策略

2020-02-10 09:35:18

數(shù)據(jù)中心服務(wù)器技術(shù)

2011-11-04 14:07:20

微軟Hotmail策略

2012-02-01 10:29:13

2017-04-12 11:15:52

ReactsetState策略

2010-09-27 09:01:26

JVM分代垃圾回收

2024-11-20 11:55:58

2024-01-04 08:33:11

異步JDK數(shù)據(jù)結(jié)構(gòu)

2024-01-02 15:41:04

CythonPython語言

2024-12-03 10:59:36

2018-10-24 14:30:30

緩存服務(wù)更新

2009-03-09 18:46:11

Windows phoWindows Mob

2023-03-14 11:00:05

過期策略Redis

2024-07-30 14:31:01

2009-02-03 09:04:51

Oracle數(shù)據(jù)庫Oracle安全策略Oracle備份

2015-10-30 09:33:48

ChromeAndroid合一

2010-11-11 14:36:17

MySQL
點贊
收藏

51CTO技術(shù)棧公眾號

99在线视频播放| 亚洲视频自拍偷拍| 久久国产精品视频在线观看| 天天干天天干天天干| 另类天堂av| 最好看的2019年中文视频| 中文写幕一区二区三区免费观成熟| 91九色美女在线视频| 久久九九影视网| 91久久伊人青青碰碰婷婷| 中文在线第一页| 午夜激情久久| 亚洲男人天天操| 又黄又爽又色的视频| 免费成人直播| 一区二区三区四区在线播放| 欧美日韩国产一二| www.精品久久| 蜜臂av日日欢夜夜爽一区| 欧美极品欧美精品欧美视频| 中文字幕在线观看二区| 牛牛影视一区二区三区免费看| 欧美日韩国产小视频在线观看| 91免费黄视频| 1区2区3区在线视频| 国产亲近乱来精品视频| 激情一区二区三区| 99精品视频免费看| 麻豆视频一区二区| 欧美怡春院一区二区三区| 欧美精品乱码视频一二专区| 爽成人777777婷婷| 亚洲香蕉伊综合在人在线视看| 国产女人18毛片水真多18| www.久久99| 精品视频在线免费观看| 青青视频在线播放| 1区2区3区在线| 一区二区三区在线免费观看 | 中文字字幕在线中文乱码| 亚洲经典在线看| 精品少妇v888av| 日韩国产第一页| 成人写真视频| 中文字幕成人在线| 91激情视频在线观看| 婷婷综合电影| 日韩精品在线免费播放| 波多野吉衣在线视频| 精品视频在线一区| 制服丝袜av成人在线看| 国产亚洲视频一区| 永久免费观看精品视频| 在线播放/欧美激情| 国产三级三级看三级| 成人涩涩视频| 欧美久久久久久久久| 色91精品久久久久久久久| 日韩有码欧美| 91精品国产日韩91久久久久久| 99日在线视频| 亚洲精品不卡在线观看| 精品国产乱码久久久久久夜甘婷婷| 中文在线字幕观看| 国产一区在线电影| 亚洲精品成人久久电影| 丰满少妇在线观看资源站| 国产精品欧美日韩一区| 这里只有精品久久| 少妇aaaaa| 亚洲天堂成人| 日产精品99久久久久久| 亚洲av人无码激艳猛片服务器| 奇米一区二区三区| 91精品视频免费看| 高清毛片aaaaaaaaa片| gogo大胆日本视频一区| 日韩精品不卡| 国产精品实拍| 亚洲sss视频在线视频| 夫妻免费无码v看片| 亚洲成av在线| 在线综合视频播放| av av在线| 国产精品一国产精品| 久久精品一区中文字幕| 五月天综合在线| 日日骚欧美日韩| 亚洲一区二区自拍| 日本亚洲欧美| 自拍偷拍亚洲综合| 伊人成色综合网| 久久精品 人人爱| 精品国产乱码久久久久久免费| 国产女主播喷水高潮网红在线| 四虎成人av| 97香蕉久久超级碰碰高清版 | 欧美激情20| 欧美老肥妇做.爰bbww| 在线观看免费视频国产| 波多野结衣的一区二区三区| 欧美黄色片在线观看| 99久久久无码国产精品免费蜜柚| 国产在线视频一区二区三区| 九九九九九九精品| 超鹏97在线| 日本乱人伦aⅴ精品| 天天操精品视频| 九九综合久久| 欧美—级a级欧美特级ar全黄 | 久久se这里有精品| 精品国产乱码久久久久软件| 黄网站视频在线观看| 一本色道久久加勒比精品| 俄罗斯女人裸体性做爰| 成人羞羞视频在线看网址| 韩国v欧美v日本v亚洲| 亚洲一区精品在线观看| av在线这里只有精品| 一本二本三本亚洲码| 日本欧美韩国| 亚洲欧美制服丝袜| 国产精品suv一区二区| 狠狠色综合日日| 日韩精品久久一区二区三区| 爱啪视频在线观看视频免费| 日韩一区二区电影在线| 欧美性生给视频| 日韩综合在线视频| 久久这里精品国产99丫e6| 欧美hdxxxxx| 欧美一区二区三区在线电影| 亚洲天堂精品一区| 日韩电影在线免费观看| 欧美成人一区二区在线| 麻豆蜜桃在线观看| 亚洲国产精品嫩草影院久久| 精品人妻在线播放| 国产精品一级在线| 亚洲av综合色区| 国产亚洲精aa在线看| 色悠悠久久88| 亚洲综合精品视频| 国产精品毛片a∨一区二区三区| 成人黄色片视频| 亚洲动漫精品| 欧洲亚洲免费在线| 日韩a在线看| 色综合色狠狠综合色| jizz日本免费| 午夜亚洲精品| 欧日韩一区二区三区| 综合久久2023| 亚洲性夜色噜噜噜7777| 国产精品无码粉嫩小泬| 国产精品福利一区二区| 男人午夜视频在线观看| 久久精品青草| 成人国产1314www色视频| 日韩激情av| 亚洲第一二三四五区| 国产精品一区二区6| 久久久五月婷婷| 91极品尤物在线播放国产| 欧美gayvideo| av一区和二区| 亚洲永久av| 一本色道久久综合狠狠躁篇的优点| 男人的天堂av网站| 国产精品免费看片| 26uuu国产| 在线亚洲欧美| 色播五月综合| 97精品资源在线观看| 美日韩在线视频| 少妇精品高潮欲妇又嫩中文字幕 | 中文字幕不卡的av| 亚洲综合20p| 99成人在线| 亚洲欧美日韩不卡一区二区三区| 国产欧美88| 午夜精品福利在线观看| 国产高清一级毛片在线不卡| 欧美一区二区视频在线观看| 日本一区二区欧美| 国产欧美日韩中文久久| 美女又黄又免费的视频| 国产一区二区三区成人欧美日韩在线观看 | 欧美在线激情网| 在线视频自拍| 欧美精品一区二区三区一线天视频| 无码人妻精品一区二区| 成人免费一区二区三区视频| 中文字幕乱视频| 日韩高清一区在线| 久久亚洲a v| 欧美三级三级| 国产欧美一区二区三区不卡高清| 78精品国产综合久久香蕉| 欧美激情一区二区三区成人| 阿v免费在线观看| 亚洲成人精品视频| 亚洲一区在线观| 色哟哟在线观看一区二区三区| 黑人狂躁日本娇小| 99久久精品免费观看| 手机av在线网| 久久先锋影音| 日本一本中文字幕| 99久久九九| 日本一区不卡| 免费日韩一区二区三区| 91嫩草在线| jvid一区二区三区| 欧美一区二区三区图| 欧美人与性动交α欧美精品图片| 自拍偷拍免费精品| 免费av在线电影| 亚洲激情电影中文字幕| 国产肥老妇视频| 欧美日韩在线不卡| 中文字字幕在线中文| 亚洲图片一区二区| 久久久久亚洲av无码专区体验| 中文字幕精品综合| 免费a级黄色片| 北条麻妃一区二区三区| 久久综合桃花网| 精彩视频一区二区| 污片在线免费看| 日韩精品视频网站| 欧美一级黄色片视频| 国产精品日韩久久久| 国产高清av在线播放| 欧美日韩99| 日本成人在线不卡| 亚洲精品99| 裸体裸乳免费看| 亚洲精品一区二区在线看| 婷婷精品国产一区二区三区日韩| 国产成人三级| 日韩在线三级| 欧美精品一区二区久久| 少妇特黄a一区二区三区| 欧美人与牛zoz0性行为| 视频一区视频二区视频三区高| 国产乱码精品一区二区亚洲| 日韩久久不卡| 日韩欧美中字| av动漫免费观看| 女同性一区二区三区人了人一| 中文字幕一区二区三区在线乱码| 99久久99久久精品国产片桃花| 一区二区三区免费| 性活交片大全免费看| 国产成人8x视频一区二区| 欧美性猛交xx| av一区二区三区| 日韩精品无码一区二区三区久久久 | 色综合久久天天| 岛国av中文字幕| 在线观看国产一区二区| 在线免费看毛片| 日韩欧美你懂的| 天天干天天爱天天操| 亚洲欧美中文日韩在线| 亚洲 小说区 图片区| 777色狠狠一区二区三区| av网站免费大全| 亚洲国产精品网站| 国产三级在线免费观看| 日韩视频免费大全中文字幕| 欧美日韩经典丝袜| 亲子乱一区二区三区电影| jizzjizz少妇亚洲水多| 91gao视频| 亚洲+变态+欧美+另类+精品| 手机在线观看国产精品| 午夜久久影院| 欧美精品一区二区三区免费播放| 美女爽到高潮91| 亚洲国产精品狼友在线观看| 国产调教视频一区| 成人免费视频国产免费观看| 亚洲va欧美va天堂v国产综合| 国产免费a视频| 日韩三级在线观看| 经典三级在线| 久久99热精品这里久久精品| 欧美一区 二区 三区| 91情侣偷在线精品国产| 网曝91综合精品门事件在线| 亚洲美女搞黄| 一本综合精品| 欧美日韩久久婷婷| 26uuu精品一区二区三区四区在线 26uuu精品一区二区在线观看 | 91视频.com| 亚洲女人久久久| 黑人巨大精品欧美一区免费视频 | 色婷婷综合成人av| 麻豆mv在线看| av激情久久| 99久久精品网| 成人性视频欧美一区二区三区| 国产福利一区在线观看| 日韩av在线看免费观看| 一区二区成人在线| 在线观看国产精品入口男同| 亚洲国内精品视频| 大片免费在线看视频| 国产成人高清激情视频在线观看| 一区二区中文字幕在线观看| 色噜噜色狠狠狠狠狠综合色一| 亚洲福利一区| 中国男女全黄大片| 中文字幕欧美一| 久久国产香蕉视频| 亚洲人成电影在线播放| 欧美激情成人动漫| 成人在线播放av| 欧美久久精品一级c片| 男人天堂1024| 成人激情动漫在线观看| 三级黄色录像视频| 在线观看区一区二| 国产永久免费高清在线观看| 欧美在线影院在线视频| 国产精品高潮呻吟久久久久| 妺妺窝人体色www看人体| 精油按摩中文字幕久久| 成人做爰69片免网站| 日本韩国欧美一区二区三区| 性感美女一级片| 97视频免费在线看| 久久精品色综合| 国产www免费| 成人app下载| 97人人澡人人爽人人模亚洲| 亚洲大胆人体在线| av老司机在线观看| 国内精品**久久毛片app| 影音先锋久久精品| 国产女人18毛片水真多18| 亚洲成a人片综合在线| 亚洲免费一级片| 久久免费视频网| 国产精品三p一区二区| 日本丰满少妇xxxx| 99视频在线精品| 毛片基地在线观看| 亚洲欧美日韩高清| 日本在线视频一区二区| 翔田千里亚洲一二三区| 久久精品国产在热久久| 久久99久久99精品免费看小说| 欧美精品久久99久久在免费线| 二区三区在线观看| 岛国视频一区| 国产精品人人爽人人做我的可爱| 午夜理伦三级做爰电影| 在线观看91视频| 日p在线观看| 91久久精品www人人做人人爽| 国自产拍偷拍福利精品免费一| www国产视频| 在线免费亚洲电影| 黄色网址免费在线观看| 99久久自偷自偷国产精品不卡| 日韩视频一区| 精品人伦一区二区| 欧美一区二区在线播放| 国产社区精品视频| 日本黄网免费一区二区精品| 精品亚洲porn| 精品欧美一区二区三区免费观看| 亚洲三级黄色在线观看| 中文幕av一区二区三区佐山爱| 国产欧美久久久久| www亚洲一区| 国产精品久久久久久久免费 | 国产麻豆电影在线观看| 成人免费视频视频在线观看免费| 亚洲天堂一区在线观看| 最近2019年好看中文字幕视频| av在线亚洲色图| 亚洲 中文字幕 日韩 无码| 亚洲欧美国产三级| 三级国产在线观看| 成人观看高清在线观看免费| 亚洲成人直播| 国产精品一区二区亚洲| 精品国产不卡一区二区三区| 日韩av一级| 男人日女人视频网站| 国产精品久久久久桃色tv| 日韩一区免费视频| 成人免费视频97| 玖玖在线精品| 精品一区在线视频| 中文字幕在线观看亚洲|