你在guava cache上设置的更新参数是否有用?

guava cache是一种支持自动回收、刷新的concurrentHashMap。显然防止内存溢出是其核心功能,包括主动刷新refresh、过期失效expire、java的软/虚引用以及设置最大的size和weight。在实际开发中,个人对于expire和refresh使用较多,expire又分为expireAfterAccess、expireAfterWrite,现对比下三者的区别:

expireAfterAccess expireAfterWrite refreshAfterWrite
功能 读写后回收 写后回收 定时刷新
更新方法 load load 首次load,其他reload
触发线程 读取线程 读取线程 读取线程+异步线程(如果重写reload使用额外线程,默认没异步)
并行非更新线程同步 通过ReentrantLock加锁,其他线程等待加载完成 通过ReentrantLock加锁 ,其他线程等待加载完成 不加锁,其他线程返回旧值
高并发读取问题 可能造成同一key永不过期 - -
高并发更新问题 频繁上锁解锁导致性能问题 频繁上锁解锁导致性能问题 -
低频读取问题 - - refresh并非异步线程定时刷新,而是由请求线程触发,在低频访问下,某个key的value可能是较早之前读取留下的,距离现在已久,会读取到较老的值

综上所看,expire和refresh都各有优缺点,refresh在低频访问可能获取到一个较老的值,而expire在高频率的收回时候因为每个线程都会加锁解锁而有性能问题。实际开发中,将两者综合使用,通过各自的优点屏蔽掉对方的缺点。比如设定refresh为1分钟一次,expireAfterWrite为2分钟一次,那么在访问高峰下,大部分会通过refresh的异步线程触发更新,避免了加锁。而当程序处于访问低谷时候,通过expire设置最大有效期为2分钟回收,后进来的线程发现已被回收再通过加锁的方式读取新值,因为量小,加锁的性能问题也可以很好的规避。

源码

可能有些同学会问,如果refresh和expireAfterWrite的更新频率都设置成一样的话,比如都是1分钟更新,那究竟是通过哪一种方式来更新呢?

答案是通过expire来更新。通过下面简化版的源码分析原因。

//与concurrentHashMap一样,获取值时候传入key与计算key的hash
V get(K key,int hash )  {
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            //获取当前时间
            long now = map.ticker.read();
            //获取存活的值,即判断是否有过期的值
            V value = getLiveValue(e, now);
            if (value != null) {
              //不存在过期,判断是否符合refresh条件触发执行refresh
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            //已回收的或者新的值时候,val都是null,此时统一key其他线程访问需要等待
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }

        // at this point e is either null or expired;
        // 不是空就是过期才会调到这个方法,即首次或者过期会加锁读取
        return lockedGetOrLoad(key, hash, loader);
    }

可以看出,guava是先判断是否有存活live的对象,如果有存活,才会进行refresh操作,如果没有存活,可能的情况是expire造成的回收或者从未有过值,这时候进行上锁lock和load取新的值,竞争的其他线程则wait等待结果。所以refresh设置的更新频率大于expire,即refresh的更新更频繁,否则refresh的设置相当于无效。

实验

为此我们做出下面的实验,通过expire调用load,refresh非首次调用reload的特性,验证上面的结论。

 @Test
    public void testExpireAndRefresh() {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executor);

        AtomicInteger count = new AtomicInteger(0);
        LoadingCache<String, Integer> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String key) throws Exception {
                        return count.getAndIncrement();
                    }

                    @Override
                    public ListenableFuture<Integer> reload(String key, Integer oldValue) throws Exception {
                        return listeningExecutorService.submit(()-> count.getAndIncrement()+10000);
                    }
                });
        IntStream.range(0, 10).forEach(i -> {
            System.out.println(cache.getUnchecked(""));
            ThreadUtil.safeSleep(1000);
        });
    }

执行结果如下,正如我们所料,调用都是load方法。

0
1
2
3
4
5
6
7
8
9

Process finished with exit code 0

同样,我们调用expire和refresh都设置成1,还是以上的结果。接着我们设置成refresh频率更高的情况

.expireAfterWrite(2, TimeUnit.SECONDS)
.refreshAfterWrite(1, TimeUnit.SECONDS)

结果也证明了原来的猜想

0
10001
10001
10002
10003
10004
10005
10006
10007
10008
Process finished with exit code 0

NOTE

除了提到的refresh和expire的更新频率问题,我们在实际开发中,最好将reload的方法异步处理,默认reload是直接调用load方法,且同步实现。因为在refresh reload后还有部分操作,异步可以并行化改块内容,加快refresh执行效率(也是官方推荐的方式)。我们可以通过CacheLoader.asyncReloading简化改部分的异步实现。

 ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());

 LoadingCache<String, Integer> cache2 = CacheBuilder.newBuilder()
                .expireAfterWrite(2, TimeUnit.SECONDS)
                .refreshAfterWrite(1, TimeUnit.SECONDS)
                .build(CacheLoader.asyncReloading(new CacheLoader<String, Integer>() {
                    @Override
                    public Integer load(String key) throws Exception {
                        return count.getAndIncrement();
                    }

                }, executor));

你可能感兴趣的:(服务器)