guava的缓存相信不少人都有用到,缓存
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100, TimeUnit.SECONDS) .maximumSize(10).build();
也经常使用的方法是设置过时时间。但使用过程当中会遇到一些问题:当过时时间到了,缓存中的对象真的会当即被释放吗?当缓存达到容量之后,如何高效的剔除缓存?guava cache的底层数据结构是如何的?带着这些问题,一块儿来看看guava cache的源码数据结构
其实经过和CurrentHashMap最类比比较好理解,只不过guava缓存在其基础上加强了缓存过时的机制:并发
答案是确定的,当咱们设置缓存用不过时(或者很长),缓存的对象不限个数(或者很大),例如框架
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100000, TimeUnit.SECONDS) .build();
不断向guava加入缓存大字符串,最终将能oom,解决这种办法:ide
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .weakValues().build();
guava在建立对象放到对应Segement中的时候,默认使用强引用(StrongValueReference.class),若是指定使用弱引用的时候,就会建立的是(WeakValueReference.class),参考guava cache基本框架可能更好理解。高并发
这个也是比较推荐的方法,根据业务需求,设置合适的缓存容量、这样超过容量之后,缓存就会按照LRU的方式回收缓存。ui
CacheBuilder.maximumSize(10)
guava清楚过时缓存的机制是什么,是单独使用线程来扫描吗?不是的,是在每次进行缓存操做的时候,如get()或者put()的时候,判断缓存是否过时。核心代码线程
void expireEntries(long now) { drainRecencyQueue(); //多线并发的状况下,防止误删access ReferenceEntry<K, V> e; while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } }
其中 writeQueue是保存按照写入缓存前后时间的队列,每次get或者put均可能触发触发这个方法。accessQueue同理,对应的是最后访问失效时间的功能。
所以能够看出,一个若是一个对象放入缓存之后,不在有任何缓存操做(包括对缓存其余key的操做),那么该缓存不会主动过时的。不过这种状况是极端状况下才会出现。code
在上面也说到了,是用accessQueue,这个队列的实现比较复杂。这个队列实际上是按照最久未使用的顺序存放的缓存对象(ReferenceEntry)的。因为会常常进行元素的移动,例如把访问过的对象放到队列的最后。ReferenceEntry这个在前面框架图里面说到了,使用来保存key-val的,其中接口包含一些特殊方法:对象
@Override public ReferenceEntry<K, V> getNextInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setNextInAccessQueue(ReferenceEntry<K, V> next) { throw new UnsupportedOperationException(); } @Override public ReferenceEntry<K, V> getPreviousInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) { throw new UnsupportedOperationException(); }
这样经过ReferenceEntry就能够判断该entry的在accessQueue中的先后节点,若是该entry不在队列中,则返回一个NullEntry的对象。这样作的好处就弥补了 链表的缺点
而且能够很方便的更新和删除链表中的节点,由于每次访问的时候均可能须要更新该链表,放入到链表的尾部,这样,每次从access中拿出的头节点就是最久未使用的。 而且,若是按照访问时间来删除缓存的时候,只要从队列里找出第一个访问没有超时的对象,那么以前遍历的缓存都是应该删除的,这样就不须要遍历整个缓存的对象来判断。
对应的writeQueue用来保存最久未更新的缓存队列,实现方式和accessQueue同样。
能够看出,guava缓存的原型是CurrentHashMap,在其基础上考虑若是判断缓存是否过时。底层的一些数据结构也是用的十分巧妙。若是能仔细的看看源码,相信对你也有必定的帮助