官方文档地址: https://github.com/google/guava/wiki/CachesExplained#applicability
使用
1. 构造
- LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
- .maximumSize(1000)
- .expireAfterWrite(10, TimeUnit.MINUTES)
- .removalListener(MY_LISTENER)
- .build(
- new CacheLoader<Key, Graph>() {
- // 必须实现
- public Graph load(Key key) throws AnyException {
- return createExpensiveGraph(key);
- }
- // 可以实现 loadAll, reload
- });
2. 获取
get
会要求你 try 住 load 中的异常
- try {
- return graphs.get(key);
- } catch (ExecutionException e) {
- throw new OtherException(e.getCause());
- }
- graphs.getUnchecked(key);
批量获取
getAll(Iterable<? extends K> keys) throws ExecutionException;
这里需要在 CacheBuilder 定义 CacheLoader 时候, 实现 loadAll
自定义 load 方法
- V get(K, Callable<V>);
- cache.get(key, new Callable<Value>() {
- @Override
- public Value call() throws AnyException {
- return doThingsTheHardWay(key);
- }
- });
3. 换出
a. 根据 maxsize 或者 maxweight 失效
- LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
- .maximumSize(100)
- .build(
- new CacheLoader<Key, Graph>() {
- public Graph load(Key key) { // no checked exception
- return createExpensiveGraph(key);
- }
- });
或者
- .maximumWeight(100000)
- .weigher(new Weigher<Key, Graph>() {
- public int weigh(Key k, Graph g) {
- return g.vertices().size();
- }
- })
不能同时设置 maxsize 和 maxweight
换出时采用 LRU 策略, accessQueue
这里有两点注意的
weight 只有在第一次 set 到 cache 的时候计算, 不会根据 value,key 变化而变化
不是严格按照 maxsize 设置的
因为 LoadingCache 是类似 ConcurrentHashmap1.6 版本的, 分成很多个 Segment 维护.
初始化的时候, 会给每个 Segment 维护一个 maxSize, 换出是每个 Segment 维护的
b. 根据时间过期或者刷新
- expireAfterAccess(long, TimeUnit)
- expireAfterWrite(long, TimeUnit)
- refreshAfterWrite(long, TimeUnit)
c. 根据对象引用失效过期
- CacheBuilder.weakKeys()
- CacheBuilder.weakValues()
- CacheBuilder.softValues()
d. 换出监听方法
- CacheBuilder
- .removalListener(new RemovalListener<String, InnerValue>() {
- @Override
- public void onRemoval(RemovalNotification<String, InnerValue> notification) {
- System.out.println("remove" + notification.getKey());
- }
- })
4. 清理
清理时机失效 meta:
当进行写操作的时候
或者如果一直没有写操作(写操作后 64 次读操作), 会由读操作来 cleanup
如果希望有一个后台线程定时清理, 可以单独其一个线程定时调用 Cache.cleanUp()
5. 源码阅读
基本结构和 ConcurrentHashMap1.6 版本的类似, 使用 Segment, 之后再下分为不同的节点
引用被 gc 失效
为了支持虚指针, 软指针回收
Entry 在 hashMap 中就是只需要保存 Key,Value
这里 Key,Value 都可能被回收调, 所以都用 Reference 对象
并且维护 gc 后的队列, 来用于 gc
- keyReferenceQueue
- valueReferenceQueue
而需要通过 value 找到对应的节点, 所以每个 value 需要维护一个指向 entry 的指针
支持失效时间和 LRU 换出
维护 3 个队列
- Queue<ReferenceEntry<K, V>> writeQueue; 最近写的 elements
- Queue<ReferenceEntry<K, V>> accessQueue; 最近访问的 elements
- Queue<ReferenceEntry<K, V>> recencyQueue 最近读取的队列
前两个队列都是双向链表 (最后连成一个环) 实现
每一个 Entry 都维护前后节点, 最新访问和最新写入的 element 放在最后
非线程安全
只有加锁之后才能操作
recencyQueue 是一个 ConcurrentLinkedQueue.
有读操作的时候, 会 add 到这个 queue
当需要 clear, 或者写入的时候, 会把 recencyQueue 中的数据添加到 accessQueue 队列中, 再进行处理
加载过程
调用 load 的时候, 会加锁, 然后生成一个 LoadingValueReference 临时节点放在 table 里
而如果 refrsh 的时候, 会先返回之前的值
load 结束之后, 会 entry 中的 value 修改为真实的 reference
- 其他线程
而 load 过程中, 其他的线程 get 时会调用 waitForLoadingValue
相当于调用了 Future.get, 这个 future 是 load 返回的 Future, 在 Future 结束之后会把线程换气
- V waitForLoadingValue(ReferenceEntry<K, V> e, K key, ValueReference<K, V> valueReference)
- throws ExecutionException {
- ***
- V value = valueReference.waitForValue();
- ***
- }
- public V waitForValue() throws ExecutionException {
- return getUninterruptibly(futureValue);
- }
- public static <V> V getUninterruptibly(Future<V> future) throws ExecutionException {
- ***
- return future.get();
- ***
- }
refresh 的问题
refresh 会调用异步 loadAsync, 会调用 CacheLoader 的 reload, 默认 reload 也是同步调用 load, 所以没有异步逻辑
可以实现 reload, 使用其他的线程来返回 ListenableFuture
- new CacheLoader<String, InnerValue>() {
- @Override
- public InnerValue load(String key) throws Exception {
- InnerValue result = new InnerValue();
- result.value = ""+ key +"_" + System.currentTimeMillis();
- return result;
- }
- @Override
- public ListenableFuture<InnerValue> reload(String key, InnerValue oldValue) throws Exception {
- return service.submit(new Callable<InnerValue>() {
- @Override
- public InnerValue call() throws Exception {
- Thread.sleep(1000);
- return load(key);
- }
- });
- }
- });
这样调用 refresh 的时候, 不会卡住主线程.
注意:
如果调用 refresh 后, 有其他线程设置了值的话, refresh 线程在执行结束后, 不会再次覆盖该值
如果 refresh 后, 有其他线程删除了这个值, 或者这个 entry 已经被 gc 了, refresh 线程在执行结束后, 还是会把这个值添加到 table 里
其他
Spring5 的本地 cache, 改用了 Caffeine https://baijiahao.baidu.com/s?id=1565651081655610&wfr=spider&for=pc
来源: https://juejin.im/entry/5ad32ff1f265da23867055b6