網(wǎng)站優(yōu)化 前端怎么做營銷模式100個經(jīng)典案例
Guava LocalCache源碼分析:LocalCache的get、put、expand
- 前言
- 一、get
- 二、put
- 三、expand
前言
上篇文章,詳細描寫了Guava LocalCache怎樣如ConcurrentHashMap對緩存數(shù)據(jù)進行了分段存儲。本章主要針對LocalCache重要的幾個接口進行說明。
一、get
@CanIgnoreReturnValue@Override@CheckForNullpublic V get(@CheckForNull Object key) {if (key == null) {return null;}int hash = hash(key);return segmentFor(hash).get(key, hash);}@CanIgnoreReturnValueV get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {int hash = hash(checkNotNull(key));return segmentFor(hash).get(key, hash, loader);}
如上代碼,LocalCache的get方法首先根據(jù)key計算出hash值,并根據(jù)hash值找到對應的segment,再調(diào)用segment的get方法獲取最終結果。
以傳入loader參數(shù)的get方法為例,看一下segment如何獲取值的。
@CanIgnoreReturnValueV get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {checkNotNull(key);checkNotNull(loader);try {if (count != 0) {//不要調(diào)用getLiveEntry,這將忽略正在加載的值//根據(jù)key和hash獲取值ReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {//獲取當前時間long now = map.ticker.read();//判斷該值是否過期V value = getLiveValue(e, now);//如果未過期if (value != null) {//記錄讀取時間recordRead(e, now);//累計命中+1,這里Guava似乎認為當多條線程在更新統(tǒng)計數(shù)據(jù)時,//而不是細粒度同步控制的情況下,LongAdder比AtomicLong更好用。statsCounter.recordHits(1);//檢查是否需要刷新,如果設置了刷新時長且過了刷新時長,則刷新,否則返回該值return scheduleRefresh(e, key, hash, value, now, loader);}//值已經(jīng)過期ValueReference<K, V> valueReference = e.getValueReference();//如果增在加載if (valueReference.isLoading()) {//等待并返回加載后的值return waitForLoadingValue(e, key, valueReference);}}}//segment中為空或者未獲取到值//加鎖,嘗試從加載中的值中獲取,若獲取不到則調(diào)用load方法。return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {Throwable cause = ee.getCause();if (cause instanceof Error) {throw new ExecutionError((Error) cause);} else if (cause instanceof RuntimeException) {throw new UncheckedExecutionException(cause);}throw ee;} finally {//累計到一定讀取次數(shù)后,清理超時緩存postReadCleanup();}}
相關調(diào)用邏輯如圖所示:
二、put
public V put(K key, V value) {checkNotNull(key);checkNotNull(value);int hash = hash(key);return segmentFor(hash).put(key, hash, value, false);}
同樣,LocalCache的put方法也是首先根據(jù)key計算出hash值,并根據(jù)hash值找到對應的segment,再調(diào)用segment的put方法。
V put(K key, int hash, V value, boolean onlyIfAbsent) {//直接加鎖lock();try {long now = map.ticker.read();//清理過期緩存preWriteCleanup(now);//數(shù)量+1int newCount = this.count + 1;if (newCount > this.threshold) {//擴容expand();//因為擴容后的segment內(nèi)的緩存數(shù)量可能會變化,所以重新計算newCount = this.count + 1;}//找到要插入的位置,并獲取該位置的頭節(jié)點AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);//尋找該key是否存在for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {K entryKey = e.getKey();if (e.getHash() == hash&& entryKey != null//判斷key是否相等&& map.keyEquivalence.equivalent(key, entryKey)) {//意味著map中存在該key//獲取對應的值ValueReference<K, V> valueReference = e.getValueReference();V entryValue = valueReference.get();//如果值是空的if (entryValue == null) {++modCount;//判斷該值是否在等待刪除中if (valueReference.isActive()) {//將舊值移放入移除通知隊列中,主要是Guava Cache有移除回調(diào)機制,故不能直接移除,隊列方便用于回調(diào)通知。enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);//加入緩存setValue(e, key, value, now);//因為一刪一增,所以,數(shù)量不變newCount = this.count; // count remains unchanged} else {//加入緩存setValue(e, key, value, now);//這里不知道為啥又算了一遍,可能是再setValue中存在某些機制導致count發(fā)生了變化?newCount = this.count + 1;}this.count = newCount; // write-volatile//移除舊值evictEntries(e);return null;} else if (onlyIfAbsent) {//onlyIfAbsent為true,如果存在于map中,僅更新訪問時間recordLockedRead(e, now);return entryValue;} else {//刪除現(xiàn)有緩存,計數(shù)保持不變++modCount;enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);setValue(e, key, value, now);evictEntries(e);return entryValue;}}}//map中不存在,則插入++modCount;ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);setValue(newEntry, key, value, now);table.set(index, newEntry);newCount = this.count + 1;this.count = newCount; // write-volatileevictEntries(newEntry);return null;} finally {//解鎖unlock();//清除過期緩存postWriteCleanup();}}
可見,整個put過程是用了鎖保證執(zhí)行的線程安全。
三、expand
LocalCache的擴容也是對段進行的擴容,當段的大小超過閾值時,便會出發(fā)擴容(詳細見上面的put函數(shù)),段的閾值為段大小的3/4,而每次擴容段的大小會變?yōu)樵瓉淼?倍。
代碼如下:
@GuardedBy("this")void expand() {AtomicReferenceArray<ReferenceEntry<K, V>> oldTable = table;int oldCapacity = oldTable.length();//如果容量超過最大容量,直接返回if (oldCapacity >= MAXIMUM_CAPACITY) {return;}int newCount = count;//新段的大小是舊段的兩倍AtomicReferenceArray<ReferenceEntry<K, V>> newTable = newEntryArray(oldCapacity << 1);//閾值為新段大小的3/4threshold = newTable.length() * 3 / 4;//因為段擴容,所以要重新計算哈希映射int newMask = newTable.length() - 1;for (int oldIndex = 0; oldIndex < oldCapacity; ++oldIndex) {//我們需要保證對舊map的任何現(xiàn)有讀取都可以繼續(xù)進行。所以我們還不能清空舊段。ReferenceEntry<K, V> head = oldTable.get(oldIndex);if (head != null) {ReferenceEntry<K, V> next = head.getNext();int headIndex = head.getHash() & newMask;// Single node on listif (next == null) {//對于單個的節(jié)點的,直接插入新段中newTable.set(headIndex, head);} else {//重復使用列表末尾具有相同目標索引的連續(xù)節(jié)點序列。tail指向可重用序列中的第一個節(jié)點。ReferenceEntry<K, V> tail = head;int tailIndex = headIndex;for (ReferenceEntry<K, V> e = next; e != null; e = e.getNext()) {int newIndex = e.getHash() & newMask;if (newIndex != tailIndex) {tailIndex = newIndex;tail = e;}}//將可重用序列中的第一個節(jié)點放入新映射位置newTable.set(tailIndex, tail);//將頭尾之間的節(jié)點重新映射到新段中for (ReferenceEntry<K, V> e = head; e != tail; e = e.getNext()) {int newIndex = e.getHash() & newMask;ReferenceEntry<K, V> newNext = newTable.get(newIndex);//拷貝當前節(jié)點作為新的頭節(jié)點,并將映射位置的頭節(jié)點鏈接到當前節(jié)點e的next。ReferenceEntry<K, V> newFirst = copyEntry(e, newNext);if (newFirst != null) {//如果拷貝的新頭節(jié)點不為null,則set到新段中newTable.set(newIndex, newFirst);} else {//否則,刪除eremoveCollectedEntry(e);newCount--;}}}}}table = newTable;this.count = newCount;}
其中,對段節(jié)點的重映射邏輯如圖所示:
這里需要留意的是Cache在重映射時,是將后續(xù)節(jié)點作為頭節(jié)點插入到?jīng)_突位中,即首插入。故,新表映射的鏈路順序與舊表會有比較大的區(qū)別。