你在guava cache上設置的更新參數是否有用?

guava cache是一種支持自動回收、刷新的concurrentHashMap。顯然防止內存溢出是其核心功能,包括主動刷新refresh、過期失效expire、java的軟/虛引用以及設置最大的size和weight。在實際開發中,個人對於expire和refresh使用較多,expire又分爲expireAfterAccess、expireAfterWrite,現對比下三者的區別:

expireAfterAccess expireAfterWrite refreshAfterWrite
功能 讀寫後回收 寫後回收 定時刷新
更新方法 load load 首次load,其他reload
觸發線程 讀取線程 讀取線程 讀取線程+異步線程(如果重寫reload使用額外線程,默認沒異步)
並行非更新線程同步 通過ReentrantLock加鎖,其他線程等待加載完成 通過ReentrantLock加鎖 ,其他線程等待加載完成 不加鎖,其他線程返回舊值
高併發讀取問題 可能造成同一key永不過期 - -
高併發更新問題 頻繁上鎖解鎖導致性能問題 頻繁上鎖解鎖導致性能問題 -
低頻讀取問題 - - refresh並非異步線程定時刷新,而是由請求線程觸發,在低頻訪問下,某個key的value可能是較早之前讀取留下的,距離現在已久,會讀取到較老的值

綜上所看,expire和refresh都各有優缺點,refresh在低頻訪問可能獲取到一個較老的值,而expire在高頻率的收回時候因爲每個線程都會加鎖解鎖而有性能問題。實際開發中,將兩者綜合使用,通過各自的優點屏蔽掉對方的缺點。比如設定refresh爲1分鐘一次,expireAfterWrite爲2分鐘一次,那麼在訪問高峯下,大部分會通過refresh的異步線程觸發更新,避免了加鎖。而當程序處於訪問低谷時候,通過expire設置最大有效期爲2分鐘回收,後進來的線程發現已被回收再通過加鎖的方式讀取新值,因爲量小,加鎖的性能問題也可以很好的規避。

源碼

可能有些同學會問,如果refresh和expireAfterWrite的更新頻率都設置成一樣的話,比如都是1分鐘更新,那究竟是通過哪一種方式來更新呢?

答案是通過expire來更新。通過下面簡化版的源碼分析原因。

//與concurrentHashMap一樣,獲取值時候傳入key與計算key的hash
V get(K key,int hash )  {
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            //獲取當前時間
            long now = map.ticker.read();
            //獲取存活的值,即判斷是否有過期的值
            V value = getLiveValue(e, now);
            if (value != null) {
              //不存在過期,判斷是否符合refresh條件觸發執行refresh
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            //已回收的或者新的值時候,val都是null,此時統一key其他線程訪問需要等待
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }

        // at this point e is either null or expired;
        // 不是空就是過期纔會調到這個方法,即首次或者過期會加鎖讀取
        return lockedGetOrLoad(key, hash, loader);
    }

可以看出,guava是先判斷是否有存活live的對象,如果有存活,纔會進行refresh操作,如果沒有存活,可能的情況是expire造成的回收或者從未有過值,這時候進行上鎖lock和load取新的值,競爭的其他線程則wait等待結果。所以refresh設置的更新頻率大於expire,即refresh的更新更頻繁,否則refresh的設置相當於無效。

實驗

爲此我們做出下面的實驗,通過expire調用load,refresh非首次調用reload的特性,驗證上面的結論。

 @Test
    public void testExpireAndRefresh() {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executor);

        AtomicInteger count = new AtomicInteger(0);
        LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String key) throws Exception {
                        return count.getAndIncrement();
                    }

                    @Override
                    public ListenableFuture<Integer> reload(String key, Integer oldValue) throws Exception {
                        return listeningExecutorService.submit(()-> count.getAndIncrement()+10000);
                    }
                });
        IntStream.range(0, 10).forEach(i -> {
            System.out.println(cache.getUnchecked(""));
            ThreadUtil.safeSleep(1000);
        });
    }

執行結果如下,正如我們所料,調用都是load方法。

0
1
2
3
4
5
6
7
8
9

Process finished with exit code 0

同樣,我們調用expire和refresh都設置成1,還是以上的結果。接着我們設置成refresh頻率更高的情況

.expireAfterWrite(2, TimeUnit.SECONDS)
.refreshAfterWrite(1, TimeUnit.SECONDS)

結果也證明了原來的猜想

0
10001
10001
10002
10003
10004
10005
10006
10007
10008
Process finished with exit code 0

NOTE

除了提到的refresh和expire的更新頻率問題,我們在實際開發中,最好將reload的方法異步處理,默認reload是直接調用load方法,且同步實現。因爲在refresh reload後還有部分操作,異步可以並行化改塊內容,加快refresh執行效率(也是官方推薦的方式)。我們可以通過CacheLoader.asyncReloading簡化改部分的異步實現。

 ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());

 LoadingCache<String, Integer> cache2 = CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(CacheLoader.asyncReloading(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String key) throws Exception {
                        return count.getAndIncrement();
                    }

                }, executor));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章