Guava Cache使用简介


1. 概述

缓存是我们日常开发中是必不可少的一种解决性能问题的方法。早期缓存只应用在CPU和内存之间,现在遍布在每一个角落:内存和磁盘,磁盘和网路都存在缓存。缓存同样是做Java应用必不可少的元素。

Cache(缓存)在很广泛的场景下都是很有用的。比如,当一个值的计算或者检索的代价很大,并且在稍后的特定输入发生后,你将不止一次的需要这个值的时候,就应当考虑使用Cache了。

Cache是和ConcurrentMap很相像的东西。最本质的不同就是,ConcurrentMap持有所有被添加进的元素,直到它们专门被移除。从另一方面来说,为了使得内存的使用可控,Cache通常来说是可配置来自动回收元素的。在一些情况下,即使LoadingCache也会很有用,虽然由于他的自动缓存加载机制,它不回收元素。

注意:如果你不需要Cache的一些特性,那么ConcurrentHashMap是更加有内存效率的--但是使用任何旧的ConcurrentMap都会很难或者几乎不可能去完成Cache所支持的一些特性。

通常,Guava Cache组件在下面的场景中适用:

  • 你想花费一些内存来提高速度。
  • 你预期到一些key的值将被不止一次地被查询。
  • 你的缓存将不会需要比内存能够存储的数据更多(Guava Cache对于一个单独运行的应用来说是本地的。它们不回将数据存储在文件中或者外部服务器上。如果这不适合你的需求,那么考虑使用其它的工具,比如Memcached或者Redis)。

如果上面的这些都符合你的需要,那么Guava Cache组件将适合你!

2. Guava Cache的使用示例

使用缓存时,最常遇到的场景需要就是:

“获取缓存-如果没有-则计算”,即[get-if-absent-compute]的原子语义

具体含义:

  1. 从缓存中取;
  2. 缓存中存在该数据,直接返回。
  3. 缓存中不存在该数据,从数据源中取;
  4. 数据源中存在该数据,放入缓存,并返回。
  5. 数据源中不存在该数据,返回空。

Guava Cache有两种方式实现:

  • 一种是CacheLoader,在定义的时候就设置好缓存的源;
  • 另一种是Callable,在调用缓存的时候指定如果缓存中没有的获取的方式。

通过这两种方式创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑:

从缓存中取key 的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。

但不同的在于cacheloader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在每次get的时候自定义获取Value的方式。

2.1. 准备工作

预先准备好一个MockDB类,用来模拟缓存中没有的时候在数据库中获取。

1
2
3
4
5
6
7
8
9
10
public class MockDB {
private static Map<String, String> mockPersistence = new HashMap<String, String>() {{
this.put("github", "codedrinker");
}};

public static String loadFromPersistence(String key) {
System.out.println("load key from persistence : " + key);
return mockPersistence.get(key);
}
}

2.2. 使用CacheLoader

下面是使用CacheLoader的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.wxweven.cache.guavatest;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* CacheLoader demo
*
* @author wxweven
* @date 2017/7/13
*/
public class GuavaLoadingCache {

public static void main(String[] args) {
LoadingCache<String, Optional<String>> loadingCache = CacheBuilder
.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.removalListener(notification -> System.out.println("cache expired, remove key : " + notification.getKey()))
.build(new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String key) throws Exception {
return Optional.ofNullable(MockDB.loadFromPersistence(key));
}
});

try {
System.out.println("load from cache once : " + loadingCache.get("github").orElse("Nothing"));
Thread.sleep(2000);
System.out.println("load from cache twice : " + loadingCache.get("github").orElse("Nothing"));
Thread.sleep(2000);
System.out.println("load from cache third : " + loadingCache.get("github").orElse("Nothing"));
Thread.sleep(2000);
System.out.println("load not exist key from cache : " + loadingCache.get("email").orElse("Nothing"));
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}

我们逐行进行解释:

  • expireAfterWrite(3, TimeUnit.SECONDS):定义缓存3秒过期;
  • removalListener:用来监听当缓存里面的key被移除时候触发的事件;
  • build(new CacheLoader<String, Optional<String>>():传入一个CacheLoader类,指定缓存中没有的时候调用 CacheLoader 类的load方法(所以一般需要重写该方法);
  • Optional:当CacheLoader尝试获取数据库中不存在的数据会抛出异常,所以我们这里使用Optional可空对象处理一下。
  • Thread.sleep(2000):缓存我们设置3秒过期,所以两次Sleep以后就会重新获取数据库。

运行输出结果如下:

1
2
3
4
5
6
7
8
load key from persistence : github
load from cache once : codedrinker
load from cache twice : codedrinker
cache expired, remove key : github
load key from persistence : github
load from cache third : codedrinker
load key from persistence : email
load not exist key from cache : Nothing

证明了再第三次获取的时候因为缓存过期了,所以需要重新在MockDB获取数据。

2.3. 使用Callable

这里我们依然需要使用上面的MockDB类,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.wxweven.cache.guavatest;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* CallableCache demo
*
* @author wxweven
* @date 2017/7/13
*/
public class GuavaCallableCache {

public static void main(String[] args) {
final String key = "github";
Cache<String, Optional<String>> cache = CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS)
.removalListener(notification ->
System.out.println("cache expired, remove key : " + notification.getKey())).build();
try {
Optional<String> optional;

System.out.println("load from cache once : " + cache.get(key, () -> Optional.ofNullable(MockDB.loadFromPersistence(key))).orElse("empty"));
Thread.sleep(2000);


System.out.println("load from cache twice : " + cache.get(key, () -> Optional.ofNullable(MockDB.loadFromPersistence(key))).orElse(null));
Thread.sleep(2000);


System.out.println("load from cache third : " + cache.get(key, () -> Optional.ofNullable(MockDB.loadFromPersistence(key))).orElse(null));
Thread.sleep(2000);

final String nullKey = "email";
optional = cache.get(nullKey, () -> Optional.ofNullable(MockDB.loadFromPersistence(nullKey)));
System.out.println("load not exist key from cache : " + optional.orElse(null));
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}

下面我们对程序进行解释:

  • 与上面例子唯一的不同就是没有在build的时候传入CacheLoader,而是在cache.get使用Cache的时候用传入Callable对象。
  • 这样做可以灵活配置每次获取的缓存源不一样,但是两种方案都各有好处,还是在使用的时候斟酌。

运行程序数据结果如下:

1
2
3
4
5
6
7
8
load key from persistence : github
load from cache once : codedrinker
load from cache twice : codedrinker
cache expired, remove key : github
load key from persistence : github
load from cache third : codedrinker
load key from persistence : email
load not exist key from cache : null

3. 回收策略

所有的cache都需要定期remove value,下面我们看看guava cache的回收策略。

3.1. 基于容量的回收(Eviction by Size)

3.1.1. maximumSize限定缓存最大容量

我们可以通过maximumSize()方法限制cache的size,如果cache达到了最大限制,默认情况下,Guava将会回收最老的缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.wxweven.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;

/**
* Guava Cache 测试类
*
* @author wxweven
* @date 2017/7/13
*/
public class GuavaCacheTest {

private static CacheLoader<String, String> loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
System.out.printf("loading cache:%s...\n", key);
return key.toUpperCase();
}
};

public void testMaximumSize() {
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.maximumSize(3) // 限制Cache的最大容量为3
.removalListener(notification -> System.out.printf("key:%s removed \n", notification.getKey()))
.build(loader);

System.out.printf("缓存大小key:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.printf("第二次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "second", cache.getUnchecked("second"));
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "third", cache.getUnchecked("third"));
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "forth", cache.getUnchecked("forth"));
System.out.println("---------------------");

System.out.printf("缓存大小key:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("再次获取缓存key:%s,value:%s\n", "first", cache.getIfPresent("first"));
}

程序输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
缓存大小key:0
---------------------
loading cache:first...
第一次获取缓存key:first,value:FIRST
第二次获取缓存key:first,value:FIRST
---------------------
loading cache:second...
第一次获取缓存key:second,value:SECOND
---------------------
loading cache:third...
第一次获取缓存key:third,value:THIRD
---------------------
loading cache:forth...
key:first removed
第一次获取缓存key:forth,value:FORTH
---------------------
缓存大小key:3
---------------------
再次获取缓存key:first,value:null

由于我们设定了缓存的最大大小为3,所以在获取 forth 时,Guava默认会将缓存中最老的key删除掉,即本例中的 first。当再次获取 first时,我们会发现得到的是null。

3.1.2. maximumWeight限定缓存最大容量

我们也可以通过定制weight function来限制cache size,以下为自定义weight function实现的限制cache size 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
public void testMaximumWeight() {
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.maximumWeight(11) // 限定缓存的最大“重量为16”
// 自定义缓存的“重量”:字符串的长度
.weigher((Weigher<String, String>) (key, value) -> value.length())
.removalListener(notification -> System.out.printf("key:%s removed \n", notification.getKey()))
.build(loader);

System.out.printf("缓存大小:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.printf("第二次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

System.out.printf("缓存大小:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "second", cache.getUnchecked("second"));
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "third", cache.getUnchecked("third"));
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "forth", cache.getUnchecked("forth"));
System.out.println("---------------------");

System.out.printf("缓存大小:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("再次获取缓存key:%s,value:%s\n", "first", cache.getIfPresent("first"));
}

程序输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
缓存大小:0
---------------------
loading cache:first...
第一次获取缓存key:first,value:FIRST
第二次获取缓存key:first,value:FIRST
---------------------
缓存大小:1
---------------------
loading cache:second...
第一次获取缓存key:second,value:SECOND
---------------------
loading cache:third...
key:first removed
第一次获取缓存key:third,value:THIRD
---------------------
loading cache:forth...
key:second removed
第一次获取缓存key:forth,value:FORTH
---------------------
缓存大小:2
---------------------
再次获取缓存key:first,value:null

可以看到,由于我们指定了缓存的maximumWeight11,而且是根据缓存中值的长度来计算weight,所以缓存最大能存放11个长度的字符串,所以当获取 third时容量就已经不足,就会让 first 失效。

3.2. 定时回收(Eviction by Time)

除了通过size来回收记录,我们也可以选择定时回收。
CacheBuilder提供两种定时回收的方法:

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

下面代码 将示例expireAfterAccess的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void testExpireAfterAccess() throws InterruptedException {
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
// 指定缓存 2 秒内没有被访问(读/写)就过期
.expireAfterAccess(2, TimeUnit.SECONDS)
.build(loader);

System.out.printf("缓存大小:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

Thread.sleep(3000);

System.out.printf("第二次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

Thread.sleep(3000);

System.out.printf("第一次获取缓存key:%s,value:%s\n", "second", cache.getUnchecked("second"));
System.out.println("---------------------");

System.out.printf("缓存大小:%d\n", cache.size());
}

程序输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
缓存大小:0
---------------------
loading cache:first...
第一次获取缓存key:first,value:FIRST
---------------------
loading cache:first...
第二次获取缓存key:first,value:FIRST
---------------------
loading cache:second...
第一次获取缓存key:second,value:SECOND
---------------------
缓存大小:1

可以看到,指定缓存访问(Access,读或写)过期时间为2秒,sleep 3秒后,first过期,第二次获取需要重新load;再sleep 3秒后,first又过期,从最后的缓存大小为1可以验证。

上面的例子演示的是 expireAfterAccess , 我们稍加改动,将其改为 expireAfterWrite,如下所示,然后再来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testExpireAfterWrite() throws InterruptedException {
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
// 指定缓存 2 秒内没有写就过期
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(loader);

System.out.printf("缓存大小:%d\n", cache.size());
System.out.println("---------------------");

System.out.printf("第一次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

Thread.sleep(1500);

System.out.printf("第二次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

Thread.sleep(1500);

// cache.put("first", "first-new");
System.out.printf("第三次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("first"));
System.out.println("---------------------");

Thread.sleep(1000);

System.out.printf("第一次获取缓存key:%s,value:%s\n", "first", cache.getUnchecked("test"));
System.out.println("---------------------");

System.out.printf("缓存大小:%d\n", cache.size());
}

程序输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
缓存大小:0
---------------------
loading cache:first...
第一次获取缓存key:first,value:FIRST
---------------------
第二次获取缓存key:first,value:FIRST
---------------------
loading cache:first...
第三次获取缓存key:first,value:FIRST
---------------------
loading cache:test...
第一次获取缓存key:first,value:TEST
---------------------
缓存大小:2

可以看到,第二次获取 first 没有重新加载,而第三次获取 first 重新加载了。就是因为间隔了 2 秒,first 仍然没有被重新写。

如果我们把上面代码中的注释行:

1
cache.put("first", "first-new");

去掉,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
缓存大小:0
---------------------
loading cache:first...
第一次获取缓存key:first,value:FIRST
---------------------
第二次获取缓存key:first,value:FIRST
---------------------
第三次获取缓存key:first,value:first-new
---------------------
loading cache:test...
第一次获取缓存key:first,value:TEST
---------------------
缓存大小:2

可以看到,第三次获取 first 并没有重新加载,因为在过期之前,缓存被重新写入了。

3.3. 基于引用的回收(Reference-based Eviction)

通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。

3.4. 显式清除

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()

4. 移除监听(RemovalNotification)

通过CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。

请注意,RemovalListener抛出的任何异常都会在记录到日志后被丢弃[swallowed]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void whenEntryRemovedFromCache_thenNotify() {
RemovalListener<String, String> listener;
listener = new RemovalListener<String, String>() {
@Override
public void onRemoval(RemovalNotification<String, String> n) {
if (n.wasEvicted()) {
String cause = n.getCause().name();
assertEquals(RemovalCause.SIZE.toString(), cause);
}
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.maximumSize(3)
.removalListener(listener)
.build(loader);
cache.getUnchecked("first");
cache.getUnchecked("second");
cache.getUnchecked("third");
cache.getUnchecked("last");
assertEquals(3, cache.size());
}

5. 刷新( Refresh the Cache)

刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。

如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。
重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void cache_reLoad() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
/**
* 重写reload方法可以定制自己的reload策略
* @param key
* @param oldValue
* @return
* @throws Exception
*/
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
return super.reload(key, oldValue);
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.build(loader);
}

CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果CacheLoader.refresh实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明expireAfterWrite和refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void whenLiveTimeEnd_thenRefresh() {
CacheLoader<String, String> loader;
loader = new CacheLoader<String, String>() {
@Override
public String load(String key) {
return key.toUpperCase();
}
};
LoadingCache<String, String> cache;
cache = CacheBuilder.newBuilder()
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(loader);
}

6. 处理空值(Handle null Values)

实际上Guava整体设计思想就是拒绝null的,很多地方都会执行com.google.common.base.Preconditions.checkNotNull的检查。

By default, Guava Cache will throw exceptions if you try to load a null value – as it doesn’t make any sense to cache a null.
But if null value means something in your code, then you can make good use of the Optional class as in the following example:

默认情况guava cache将会抛出异常,如果试图加载null value–因为cache null 是没有任何意义的。
但是如果null value 对你的代码而已有一些特殊的含义,你可以尝试用Optional来表达,像下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void whenNullValue_thenOptional() {
CacheLoader<String, Optional<String>> loader;
loader = new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String key) {
return Optional.fromNullable(getSuffix(key));
}
};
LoadingCache<String, Optional<String>> cache;
cache = CacheBuilder.newBuilder().build(loader);
assertEquals("txt", cache.getUnchecked("text.txt").get());
assertFalse(cache.getUnchecked("hello").isPresent());
}
private String getSuffix(final String str) {
int lastIndex = str.lastIndexOf('.');
if (lastIndex == -1) {
return null;
}
return str.substring(lastIndex + 1);
}

7. 统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

  • hitRate():缓存命中率;
  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
  • evictionCount():缓存项被回收的总数,不包括显式清除。

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

8. getUnchecked

什么时候用get,什么时候用getUnchecked
官网文档说:

  • If you have defined a CacheLoader that does not declare any checked exceptions then you can perform cache lookups using getUnchecked(K);
  • However care must be taken not to call getUnchecked on caches whose CacheLoaders declare checked exceptions.

字面意思是,如果你的CacheLoader没有定义任何checked Exception,那你可以使用getUnchecked。但是 ,一定要注意,如果CacheLoader声明了checked exceptions,那就不要调用getUnchecked。

9. 总结

在设计Java分布式应用程序的时候,针对一些基本不变的数据,或者是变化不大然而使用非常频繁的数据可以考虑采用Guava Cache实现Java应用内存级别缓存。