Guava 源碼分析(Cache 原理【二階段】)

前言

在上文「Guava 源碼分析(Cache 原理)」中分析了 Guava Cache 的相關原理。java

文末提到了回收機制、移除時間通知等內容,許多朋友也挺感興趣,此次就這兩個內容再來分析分析。git

在開始以前先補習下 Java 自帶的兩個特性,Guava 中都有具體的應用。

Java 中的引用

首先是 Java 中的引用github

在以前分享過 JVM 是根據可達性分析算法找出須要回收的對象,判斷對象的存活狀態都和引用有關。算法

在 JDK1.2 以前這點設計的很是簡單:一個對象的狀態只有引用沒被引用兩種區別。設計模式

這樣的劃分對垃圾回收不是很友好,由於總有一些對象的狀態處於這兩之間。緩存

所以 1.2 以後新增了四種狀態用於更細粒度的劃分引用關係:異步

  • 強引用(Strong Reference):這種對象最爲常見,好比 A a = new A();這就是典型的強引用;這樣的強引用關係是不能被垃圾回收的。
  • 軟引用(Soft Reference):這樣的引用代表一些有用但不是必要的對象,在將發生垃圾回收以前是須要將這樣的對象再次回收。
  • 弱引用(Weak Reference):這是一種比軟引用還弱的引用關係,也是存放非必須的對象。當垃圾回收時,不管當前內存是否足夠,這樣的對象都會被回收。
  • 虛引用(Phantom Reference):這是一種最弱的引用關係,甚至無法經過引用來獲取對象,它惟一的做用就是在被回收時能夠得到通知。

事件回調

事件回調實際上是一種常見的設計模式,好比以前講過的 Netty 就使用了這樣的設計。ide

這裏採用一個 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() 方法;其實在這裏面就是對剛纔的隊列進行了消費:

一直跟進來就會發現這裏消費了隊列,將以前包裝好的移除消息調用了咱們自定義的事件,這樣就完成了一次事件回調。

總結

以上全部源碼:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/callback/Main.java

經過分析 Guava 的源碼可讓咱們學習到頂級的設計及實現方式,甚至本身也能嘗試編寫。

Guava 裏還有不少強大的加強實現,值得咱們再好好研究。

號外

最近在總結一些 Java 相關的知識點,感興趣的朋友能夠一塊兒維護。

地址: https://github.com/crossoverJie/Java-Interview

歡迎關注公衆號一塊兒交流:

相關文章
相關標籤/搜索