前言
在上文「Guava 原始碼分析(Cache 原理)」中分析了 Guava Cache
的相關原理。
文末提到了回收機制、移除時間通知等內容,許多朋友也挺感興趣,這次就這兩個內容再來分析分析。
在開始之前先補習下 Java 自帶的兩個特性,Guava 中都有具體的應用。
Java 中的引用
首先是 Java 中的引用。
在之前分享過 JVM 是根據可達性分析演算法找出需要回收的物件,判斷物件的存活狀態都和引用
有關。
在 JDK1.2 之前這點設計的非常簡單:一個物件的狀態只有引用和沒被引用兩種區別。
這樣的劃分對垃圾回收不是很友好,因為總有一些物件的狀態處於這兩之間。
因此 1.2 之後新增了四種狀態用於更細粒度的劃分引用關係:
- 強引用(Strong Reference):這種物件最為常見,比如 **
A a = new A();
**這就是典型的強引用;這樣的強引用關係是不能被垃圾回收的。 - 軟引用(Soft Reference):這樣的引用表明一些有用但不是必要的物件,在將發生垃圾回收之前是需要將這樣的物件再次回收。
- 弱引用(Weak Reference):這是一種比軟引用還弱的引用關係,也是存放非必須的物件。當垃圾回收時,無論當前記憶體是否足夠,這樣的物件都會被回收。
- 虛引用(Phantom Reference):這是一種最弱的引用關係,甚至沒法通過引用來獲取物件,它唯一的作用就是在被回收時可以獲得通知。
事件回撥
事件回撥其實是一種常見的設計模式,比如之前講過的 Netty 就使用了這樣的設計。
這裡採用一個 demo,試下如下功能:
- Caller 向 Notifier 提問。
- 提問方式是非同步,接著做其他事情。
- Notifier 收到問題執行計算然後回撥 Caller 告知結果。
在 Java 中利用介面來實現回撥,所以需要定義一個介面:
public interface CallBackListener {
/**
* 回撥通知函式
* @param msg
*/
void callBackNotify(String msg) ;
}
複製程式碼
Caller 中呼叫 Notifier 執行提問,呼叫時將介面傳遞過去:
public class Caller {
private final static Logger LOGGER = LoggerFactory.getLogger(Caller.class);
private CallBackListener callBackListener ;
private Notifier notifier ;
private String question ;
/**
* 使用
*/
public void call(){
LOGGER.info("開始提問");
//新建執行緒,達到非同步效果
new Thread(new Runnable() {
@Override
public void run() {
try {
notifier.execute(Caller.this,question);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
LOGGER.info("提問完畢,我去幹其他事了");
}
//隱藏 getter/setter
}
複製程式碼
Notifier 收到提問,執行計算(耗時操作),最後做出響應(回撥介面,告訴 Caller 結果)。
public class Notifier {
private final static Logger LOGGER = LoggerFactory.getLogger(Notifier.class);
public void execute(Caller caller, String msg) throws InterruptedException {
LOGGER.info("收到訊息=【{}】", msg);
LOGGER.info("等待響應中。。。。。");
TimeUnit.SECONDS.sleep(2);
caller.getCallBackListener().callBackNotify("我在北京!");
}
}
複製程式碼
模擬執行:
public static void main(String[] args) {
Notifier notifier = new Notifier() ;
Caller caller = new Caller() ;
caller.setNotifier(notifier) ;
caller.setQuestion("你在哪兒!");
caller.setCallBackListener(new CallBackListener() {
@Override
public void callBackNotify(String msg) {
LOGGER.info("回覆=【{}】" ,msg);
}
});
caller.call();
}
複製程式碼
最後執行結果:
2018-07-15 19:52:11.105 [main] INFO c.crossoverjie.guava.callback.Caller - 開始提問
2018-07-15 19:52:11.118 [main] INFO c.crossoverjie.guava.callback.Caller - 提問完畢,我去幹其他事了
2018-07-15 19:52:11.117 [Thread-0] INFO c.c.guava.callback.Notifier - 收到訊息=【你在哪兒!】
2018-07-15 19:52:11.121 [Thread-0] INFO c.c.guava.callback.Notifier - 等待響應中。。。。。
2018-07-15 19:52:13.124 [Thread-0] INFO com.crossoverjie.guava.callback.Main - 回覆=【我在北京!】
複製程式碼
這樣一個模擬的非同步事件回撥就完成了。
Guava 的用法
Guava 就是利用了上文的兩個特性來實現了引用回收及移除通知。
引用
可以在初始化快取時利用:
- CacheBuilder.weakKeys()
- CacheBuilder.weakValues()
- CacheBuilder.softValues()
來自定義鍵和值的引用關係。
在上文的分析中可以看出 Cache 中的 ReferenceEntry
是類似於 HashMap 的 Entry 存放資料的。
來看看 ReferenceEntry 的定義:
interface ReferenceEntry<K, V> {
/**
* Returns the value reference from this entry.
*/
ValueReference<K, V> getValueReference();
/**
* Sets the value reference for this entry.
*/
void setValueReference(ValueReference<K, V> valueReference);
/**
* Returns the next entry in the chain.
*/
@Nullable
ReferenceEntry<K, V> getNext();
/**
* Returns the entry`s hash.
*/
int getHash();
/**
* Returns the key for this entry.
*/
@Nullable
K getKey();
/*
* Used by entries that use access order. Access entries are maintained in a doubly-linked list.
* New entries are added at the tail of the list at write time; stale entries are expired from
* the head of the list.
*/
/**
* Returns the time that this entry was last accessed, in ns.
*/
long getAccessTime();
/**
* Sets the entry access time in ns.
*/
void setAccessTime(long time);
}
複製程式碼
包含了很多常用的操作,如值引用、鍵引用、訪問時間等。
根據 ValueReference<K, V> getValueReference();
的實現:
具有強引用和弱引用的不同實現。
key 也是相同的道理:
當使用這樣的構造方式時,弱引用的 key 和 value 都會被垃圾回收。
當然我們也可以顯式的回收:
/**
* Discards any cached value for key {@code key}.
* 單個回收
*/
void invalidate(Object key);
/**
* Discards any cached values for keys {@code keys}.
*
* @since 11.0
*/
void invalidateAll(Iterable<?> keys);
/**
* Discards all entries in the cache.
*/
void invalidateAll();
複製程式碼
回撥
改造了之前的例子:
loadingCache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.removalListener(new RemovalListener<Object, Object>() {
@Override
public void onRemoval(RemovalNotification<Object, Object> notification) {
LOGGER.info("刪除原因={},刪除 key={},刪除 value={}",notification.getCause(),notification.getKey(),notification.getValue());
}
})
.build(new CacheLoader<Integer, AtomicLong>() {
@Override
public AtomicLong load(Integer key) throws Exception {
return new AtomicLong(0);
}
});
複製程式碼
執行結果:
2018-07-15 20:41:07.433 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 當前快取值=0,快取大小=1
2018-07-15 20:41:07.442 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 快取的所有內容={1000=0}
2018-07-15 20:41:07.443 [main] INFO c.crossoverjie.guava.CacheLoaderTest - job running times=10
2018-07-15 20:41:10.461 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 刪除原因=EXPIRED,刪除 key=1000,刪除 value=1
2018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 當前快取值=0,快取大小=1
2018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 快取的所有內容={1000=0}
複製程式碼
可以看出當快取被刪除的時候會回撥我們自定義的函式,並告知刪除原因。
那麼 Guava 是如何實現的呢?
根據 LocalCache 中的 getLiveValue()
中判斷快取過期時,跟著這裡的呼叫關係就會一直跟到:
removeValueFromChain()
中的:
enqueueNotification()
方法會將回收的快取(包含了 key,value)以及回收原因包裝成之前定義的事件介面加入到一個本地佇列中。
這樣一看也沒有回撥我們初始化時候的事件啊。
不過用過佇列的同學應該能猜出,既然這裡寫入佇列,那就肯定就有消費。
我們回到獲取快取的地方:
在 finally 中執行了 postReadCleanup()
方法;其實在這裡面就是對剛才的佇列進行了消費:
一直跟進來就會發現這裡消費了佇列,將之前包裝好的移除訊息呼叫了我們自定義的事件,這樣就完成了一次事件回撥。
總結
以上所有原始碼:
通過分析 Guava 的原始碼可以讓我們學習到頂級的設計及實現方式,甚至自己也能嘗試編寫。
Guava 裡還有很多強大的增強實現,值得我們再好好研究。
號外
最近在總結一些 Java 相關的知識點,感興趣的朋友可以一起維護。
歡迎關注公眾號一起交流: