Guava 源码分析(Cache 原理【二阶段】)

前言

在上文「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 中利用接口来实现回调,所以需要定义一个接口:

1public interface CallBackListener {
2
3    /**
4     * 回调通知函数
5     * @param msg
6     */
7    void callBackNotify(String msg) ;
8}

Caller 中调用 Notifier 执行提问,调用时将接口传递过去:

 1public class Caller {
 2
 3    private final static Logger LOGGER = LoggerFactory.getLogger(Caller.class);
 4
 5    private CallBackListener callBackListener ;
 6
 7    private Notifier notifier ;
 8
 9    private String question ;
10
11    /**
12     * 使用
13     */
14    public void call(){
15
16        LOGGER.info("开始提问");
17
18        //新建线程,达到异步效果 
19        new Thread(new Runnable() {
20            @Override
21            public void run() {
22                try {
23                    notifier.execute(Caller.this,question);
24                } catch (InterruptedException e) {
25                    e.printStackTrace();
26                }
27            }
28        }).start();
29
30        LOGGER.info("提问完毕,我去干其他事了");
31    }
32
33    //隐藏 getter/setter
34
35}    

Notifier 收到提问,执行计算(耗时操作),最后做出响应(回调接口,告诉 Caller 结果)。

 1public class Notifier {
 2
 3    private final static Logger LOGGER = LoggerFactory.getLogger(Notifier.class);
 4
 5    public void execute(Caller caller, String msg) throws InterruptedException {
 6        LOGGER.info("收到消息=【{}】", msg);
 7
 8        LOGGER.info("等待响应中。。。。。");
 9        TimeUnit.SECONDS.sleep(2);
10
11
12        caller.getCallBackListener().callBackNotify("我在北京!");
13
14    }
15
16}

模拟执行:

 1    public static void main(String[] args) {
 2        Notifier notifier = new Notifier() ;
 3
 4        Caller caller = new Caller() ;
 5        caller.setNotifier(notifier) ;
 6        caller.setQuestion("你在哪儿!");
 7        caller.setCallBackListener(new CallBackListener() {
 8            @Override
 9            public void callBackNotify(String msg) {
10                LOGGER.info("回复=【{}】" ,msg);
11            }
12        });
13
14        caller.call();
15    }

最后执行结果:

12018-07-15 19:52:11.105 [main] INFO  c.crossoverjie.guava.callback.Caller - 开始提问
22018-07-15 19:52:11.118 [main] INFO  c.crossoverjie.guava.callback.Caller - 提问完毕,我去干其他事了
32018-07-15 19:52:11.117 [Thread-0] INFO  c.c.guava.callback.Notifier - 收到消息=【你在哪儿!】
42018-07-15 19:52:11.121 [Thread-0] INFO  c.c.guava.callback.Notifier - 等待响应中。。。。。
52018-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 的定义:

 1  interface ReferenceEntry<K, V> {
 2    /**
 3     * Returns the value reference from this entry.
 4     */
 5    ValueReference<K, V> getValueReference();
 6
 7    /**
 8     * Sets the value reference for this entry.
 9     */
10    void setValueReference(ValueReference<K, V> valueReference);
11
12    /**
13     * Returns the next entry in the chain.
14     */
15    @Nullable
16    ReferenceEntry<K, V> getNext();
17
18    /**
19     * Returns the entry's hash.
20     */
21    int getHash();
22
23    /**
24     * Returns the key for this entry.
25     */
26    @Nullable
27    K getKey();
28
29    /*
30     * Used by entries that use access order. Access entries are maintained in a doubly-linked list.
31     * New entries are added at the tail of the list at write time; stale entries are expired from
32     * the head of the list.
33     */
34
35    /**
36     * Returns the time that this entry was last accessed, in ns.
37     */
38    long getAccessTime();
39
40    /**
41     * Sets the entry access time in ns.
42     */
43    void setAccessTime(long time);
44}

包含了很多常用的操作,如值引用、键引用、访问时间等。

根据 ValueReference<K, V> getValueReference(); 的实现:

具有强引用和弱引用的不同实现。

key 也是相同的道理:

当使用这样的构造方式时,弱引用的 key 和 value 都会被垃圾回收。

当然我们也可以显式的回收:

 1  /**
 2   * Discards any cached value for key {@code key}.
 3   * 单个回收
 4   */
 5  void invalidate(Object key);
 6
 7  /**
 8   * Discards any cached values for keys {@code keys}.
 9   *
10   * @since 11.0
11   */
12  void invalidateAll(Iterable<?> keys);
13
14  /**
15   * Discards all entries in the cache.
16   */
17  void invalidateAll();

回调

改造了之前的例子:

 1loadingCache = CacheBuilder.newBuilder()
 2        .expireAfterWrite(2, TimeUnit.SECONDS)
 3        .removalListener(new RemovalListener<Object, Object>() {
 4            @Override
 5            public void onRemoval(RemovalNotification<Object, Object> notification) {
 6                LOGGER.info("删除原因={},删除 key={},删除 value={}",notification.getCause(),notification.getKey(),notification.getValue());
 7            }
 8        })
 9        .build(new CacheLoader<Integer, AtomicLong>() {
10            @Override
11            public AtomicLong load(Integer key) throws Exception {
12                return new AtomicLong(0);
13            }
14        });

执行结果:

12018-07-15 20:41:07.433 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1
22018-07-15 20:41:07.442 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 缓存的所有内容={1000=0}
32018-07-15 20:41:07.443 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - job running times=10
42018-07-15 20:41:10.461 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 删除原因=EXPIRED,删除 key=1000,删除 value=1
52018-07-15 20:41:10.462 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 当前缓存值=0,缓存大小=1
62018-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 里还有很多强大的增强实现,值得我们再好好研究。

原文发布于微信公众号 - 纯洁的微笑(keeppuresmile)

原文发表时间:2018-07-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏禁心尽力

springmvc框架开发常用的注解总结

1、@Controller使用:表示表现层中的JavaBean被spring容器管理。 2、@requestMapping使用:   a) 在方法上: 标记ur...

23750
来自专栏Java架构师学习

带你深入了解Java线程中的那些事

引言 说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢? public class Thre...

35080
来自专栏BinarySec

ISCC中pwn200 shell无法启动原因详解

0x00 背景 一朋友问到在pwn中,gdb调试看到了systemm("/bin/sh")了,但是shell确无法启动。于是我详细看了一下这个题目,发现自己的e...

52150
来自专栏开发与安全

linux网络编程之socket(一):socket概述和字节序、地址转换函数

一、什么是socket socket可以看成是用户进程与内核网络协议栈的编程接口。 socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程...

34300
来自专栏JackieZheng

照虎画猫写自己的Spring——依赖注入

前言 上篇《照虎画猫写自己的Spring》从无到有讲述并实现了下面几点 声明配置文件,用于声明需要加载使用的类 加载配置文件,读取配置文件 解析配置文件,需要将...

21680
来自专栏Charlie's Road

<Solidity学习系列四>使用编译器

Solidity存储库的一个构建目标是solc,solidity命令行编译器。 使用solc --help为您提供所有选项的解释。 编译器可以生成各种输出,范围...

16820
来自专栏小灰灰

SpringMVC返回图片的几种方式

主要借助的是 HttpServletResponse这个对象,实现case如下

27770
来自专栏Hongten

spring开发_spring中Bean的作用域_singleton_prototype

http://www.cnblogs.com/hongten/gallery/image/112385.html

18520
来自专栏SDNLAB

Open vSwitch系列之openflow版本兼容

众所周知Open vSwitch支持的openflow版本从1.0到1.5版本(当前Open vSwitch版本是2.3.2)通过阅读代码,处理openflow...

585130
来自专栏Seebug漏洞平台

Jenkins 未授权远程代码执行漏洞(CVE-2017-1000353)

漏洞概要 Jenkins 未授权远程代码执行漏洞, 允许攻击者将序列化的Java SignedObject对象传输给Jenkins CLI处理,反序列化Obj...

42960

扫码关注云+社区

领取腾讯云代金券