1、Guava
Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。如:RateLimiter可以在高并发场景下限流操作。
-
Joiner-Spliter
Joiner
首先我们来看看下面我们经常遇见的一个案例:
题目: 对于一个如下定义List List<String> list = new ArrayList<String>("1", "2", null, “3”); 按照’,’分割,并过滤掉null。
如果不用第三方库,如common-lange,Guava,用原生java,我们将怎么继续?
public static String join(List stringList, String delimiter) { StringBuilder builder = new StringBuilder(); for (Object item : stringList) { if (item != null) { builder .append(item) .append(delimiter); } } builder.setLength(builder.length() delimiter.length()); return builder.toString(); }
是不是很简单,但是繁琐,而且这里还有个坑,我们使用append的方式,在每次for完成后,我们必须去修正remove最后的分隔符:builder.setLength(builder.length() delimiter.length());
Guava版本呢?
public static String joinByGuava(List stringList, String delimiter) { return Joiner .on(delimiter) .skipNulls() .join(stringList); }
我们不在考虑更多的细节,并且很有语义的告诉代码的阅读者,用什么分隔符,需要过滤null值再join。
note:当然我们也可以用common-lange来很简单的完成:StringUtils.join(stringList, delimiter).但是个人推荐尽量使用Guava替代common-lange,因为Guava还有更多的有用方法,后续会陆续介绍,还有就是Guava的API相对更有语意一点。
Splitter
MapJoinner和MapSplitter
对于MapJoinner和MapSplitter的最好案例就是url的param编码。
MapJoinner
题目: 生产一个查询id: 123,name: green的学生信息的url。
利用Guava的MapJoinner的代码如下:
Joiner.on("&").withKeyValueSeparator("=").join(ImmutableMap.of("id", "123", "name", "green"));
这里采用了on传入map item之间分隔符,以及withKeyValueSeparator传递map项key/value之间的分隔符。所以能够很简单的实现,不用我们在去实现一个的for循环代码。
MapSplitter
题目: 对url中的查询字符串"id=123&name=green"进行分割
利用Guava的MapSplitter的代码如下:
final Map<String, String> join = Splitter.on("&").withKeyValueSeparator("=").split("id=123&name=green");
这里同样利用on传入字符串的第一分隔符,withKeyValueSeparator传入项的分隔符,产生map的key/value项,其结果是一个{id=123, name=green}的Map对象。
-
Optinal
Null sucks
回到本文主题Optional。在我日常编程中NullPointerException是肯定是大家遇见最多的异常错误:
为此Doug Lea曾说过:
Null sucks.
Sir C. A. R. Hoare也曾说过:
I call it my billion-dollar mistake.
从上面我们能够足以看出NullPointerExceptiond的出现频率和可恨之处。因此在GOF的设计模式中我们也专门提出了空对象模式(或称特例模式)来应对这可恶的NullPointerExceptiond。空对象模式主要以返回一些无意义并不影响处理逻辑的特定对象来替代null对象,从而避免没必要的null对象的判断。 例如在计算一组员工的总共薪资的时候,对于返回的null对象则我们可以返回默认值为了0薪资的员工对象,那么我们就不需要做任何null的判断。
员工薪资问题
那么在Guava的Optional又该怎么解决呢?在讲解Optional之前,让我们仍然以计算一组员工的总共薪资为例用原生java代码将来看看:
@Test public void should_get_total_age_for_all_employees() throws Exception { final List<Employee> list = Lists.newArrayList(new Employee("em1", 30), new Employee("em2", 40), null, new Employee("em4", 18)); int sum = 0; for (Employee employee : list) { if (employee != null) { sum += employee.getAge(); } } System.out.println(sum); } private class Employee { private final String name; private final int age; public Employee(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
如果换成Guava Optional将如何:
@Test public void should_get_total_age_for_all_employees() throws Exception { final List<Employee> list = Lists.newArrayList(new Employee("em1", 30), new Employee("em2", 40), null, new Employee("em4", 18)); int sum = 0; for (Employee employee : list) { sum += Optional.fromNullable(employee).or(new Employee("dummy", 0)).getAge(); } System.out.println(sum); }
从上面可以清晰看出,我们不在担心对象对空了,利用Optional的fromNullable创建了一个可空对象,并将其or上一个dummy的员工信息,所以在这里我们不在担心NullPointerExceptiond。
也许你会说和利用三目运算 ( ?:)没什么差别,在此例子中功能是的确是没多大差距,但是个人觉得Guava更有语义,更通用一些,而且满足很多空对象模式使用的场景。
4.集合
Guava中的集合方法扩展
任何对JDK集合框架有经验的程序员都熟悉和喜欢java.util.Collections包含的工具方法。Guava沿着这些路线提供了更多的工具方法:适用于所有集合的静态方法。这是Guava最流行和成熟的部分原因之一。
集合接口 | JDK/Guava | Guava工具类 |
---|---|---|
Collection | JDK | Collections2 :不要和java.util.Collections混淆 |
List | JDK | Lists |
Set | JDK | Sets |
SortedSet | JDK | Sets |
Map | JDK | Maps |
SortedMap | JDK | Maps |
Queue | JDK | Queues |
Multiset | Guava | Multisets |
Multimap | Guava | Multimaps |
BiMap | Guava | Maps |
Table | Guava | Tables |
静态工方法
Person
:
package com.example.chapter1.guava; import lombok.AllArgsConstructor; import lombok.Data; import lombok.ToString; /** * <p> * * </p> * ... */ @Data @ToString @AllArgsConstructor public class Person { private String name; }
Ex.
:
/** * 推断范型的静态工厂方法 * - 构造新的范型集合 * - 初始化起始元素 * - 工厂法声明集合变量 * - 工厂法初始化集合大小 */ @Test public void declareStaticFactory(){ //构造新的范型集合 List<Person> list = Lists.newArrayList(); Map<String, Person> map = Maps.newLinkedHashMap(); Map<String, Person> hsahMap = Maps.newHashMap(); //... list.add(new Person("p1")); //初始化起始元素 Set<Person> copySet = Sets.newHashSet(list); System.out.println(copySet);//[Person(name=p1)] List<String> theseElements = Lists.newArrayList("alpha", "beta", "gamma"); System.out.println(theseElements);//[alpha, beta, gamma] //工厂方法命名(Effective Java第一条),我们可以提高集合初始化大小的可读性 List<Person> exactly100 = Lists.newArrayListWithCapacity(100); List<Person> approx100 = Lists.newArrayListWithExpectedSize(100); Set<Person> approx100Set = Sets.newHashSetWithExpectedSize(100); //Guava引入的新集合类型没有暴露原始构造器,也没有在工具类中提供初始化方法。而是直接在集合类中提供了静态工厂方法 Multiset<String> multiset = HashMultiset.create(); }
Iterables
常规方法
集合接口 | 描述 | 示例 |
---|---|---|
concat(Iterable<Iterable>) | 串联多个iterables的懒视图 | concat(Iterable...) |
frequency(Iterable, Object) | 返回对象在iterable中出现的次数 | 与Collections.frequency (Collection,Object)比较;Multiset |
partition(Iterable, int) | 把iterable按指定大小分割,得到的子集都不能进行修改操作 | Lists.partition(List, int);paddedPartition(Iterable, int) |
getFirst(Iterable, T default) | 返回iterable的第一个元素,若iterable为空则返回默认值 | 与Iterable.iterator().next()比较;FluentIterable.first() |
getLast(Iterable) | 返回iterable的最后一个元素,若iterable为空则抛出NoSuchElementException | getLast(Iterable, T default);FluentIterable.last() |
elementsEqual(Iterable, Iterable) | 如果两个iterable中的所有元素相等且顺序一致,返回true | 与List.equals(Object)比较 |
unmodifiableIterable(Iterable) | 返回iterable的不可变视图 | 与Collections.unmodifiableCollection(Collection)比较 |
limit(Iterable, int) | 限制iterable的元素个数限制给定值 | FluentIterable.limit(int) |
getOnlyElement(Iterable) | 获取iterable中唯一的元素,如果iterable为空或有多个元素,则快速失败 | getOnlyElement(Iterable, T default) |
>注:
懒视图意味着如果还没访问到某个iterable中的元素,则不会对它进行串联操作
/** * 在可能的情况下,Guava提供的工具方法更偏向于接受Iterable而不是Collection类型。 * 在Google,对于不存放在主存的集合(比如从数据库或其他数据中心收集的结果集) * 因为实际上还没有获取全部数据,这类结果集都不能支持类似size()的操作,通常都不会用Collection类型来表示。 */ @Test public void testGuavaIterables(){ Set<Integer> linkedHashSet = Sets.newLinkedHashSet(); linkedHashSet.add(7); Iterable<Integer> iterable = Iterables.concat(Ints.asList(1,2,3),Ints.asList(4,5,6),linkedHashSet); Integer last = Iterables.getLast(linkedHashSet); System.out.println(iterable); System.out.println(last); //[1, 2, 3, 4, 5, 6, 7] //7 Integer element = Iterables.getOnlyElement(linkedHashSet); System.out.println(element); //7 linkedHashSet.add(8); element = Iterables.getOnlyElement(linkedHashSet); System.out.println(element); //java.lang.IllegalArgumentException: expected one element but was: <7, 8> linkedHashSet 如果不是单元素就会报错! }
与Collection方法相似的工具方法
Iterables
方法 | 类似的Collection方法 | 等价的FluentIterable方法 |
---|---|---|
addAll(Collection addTo, Iterable toAdd) | Collection.addAll(Collection) | |
contains(Iterable, Object) | Collection.contains(Object) | FluentIterable.contains(Object) |
removeAll(Iterable removeFrom, Collection toRemove) | Collection.removeAll(Collection) | |
retainAll(Iterable removeFrom, Collection toRetain) | Collection.retainAll(Collection) | |
size(Iterable) | Collection.size() | FluentIterable.size() |
toArray(Iterable, Class) | Collection.toArray(T[]) | FluentIterable.toArray(Class) |
isEmpty(Iterable) | Collection.isEmpty() | FluentIterable.isEmpty() |
get(Iterable, int) | List.get(int) | FluentIterable.get(int) |
toString(Iterable) | Collection.toString() | FluentIterable.toString() |
>注:
上面的方法中,如果传入的Iterable是一个Collection实例,则实际操作将会委托给相应的Collection接口方法。例如,往Iterables.size方法传入是一个Collection实例,它不会真的遍历iterator获取大小,而是直接调用Collection.size。
//通常来说 Collection的实现天然支持操作其他Collection,但却不能操作Iterable。 List<Person> list = Lists.newArrayList(); list.add(new Person("p1")); System.out.println(Iterables.size(list));//1 源码:
/** Returns the number of elements in {@code iterable}. */ public static int size(Iterable<?> iterable) { return (iterable instanceof Collection) ? ((Collection<?>) iterable).size() : Iterators.size(iterable.iterator()); }
FluentIterable 还有一些便利方法用来把自己拷贝到 不可变集合
名称 | 方法 |
---|---|
ImmutableSet | toSet() |
ImmutableSortedSet | toImmutableSortedSet(Comparator) |
/** * FluentIterable还有一些便利方法用来把自己拷贝到不可变集合 * 为什么要用immutable对象?immutable对象有以下的优点: * 1.对不可靠的客户代码库来说,它使用安全,可以在未受信任的类库中安全的使用这些对象 * 2.线程安全的:immutable对象在多线程下安全,没有竞态条件 * 3.不需要支持可变性, 可以尽量节省空间和时间的开销. 所有的不可变集合实现都比可变集合更加有效的利用内存 (analysis) * 4.可以被使用为一个常量,并且期望在未来也是保持不变的 * Guava提供了对JDK里标准集合类里的immutable版本的简单方便的实现,以及Guava自己的一些专门集合类的immutable实现。当你不希望修改一个集合类,或者想做一个常量集合类的时候,使用immutable集合类就是一个最佳的编程实践 * 注意:每个Guava immutable集合类的实现都拒绝null值。我们做过对Google内部代码的全面的调查,并且发现只有5%的情况下集合类允许null值,而95%的情况下都拒绝null值。万一你真的需要能接受null值的集合类,你可以考虑用Collections.unmodifiableXXX。 * Immutable集合使用方法,一个immutable集合可以有以下几种方式来创建: * 1.用copyOf方法, 譬如, ImmutableSet.copyOf(set) * 2.使用of方法,譬如,ImmutableSet.of("a", "b", "c")或者ImmutableMap.of("a", 1, "b", 2) * 3.使用Builder类 * */ @Test public void testGuavaFluentIterable(){ Set<Integer> linkedHashSet = Sets.newLinkedHashSet(); linkedHashSet.add(7); ImmutableSet<Integer> immutableSet = ImmutableSet.copyOf(linkedHashSet); System.out.println(immutableSet);//[7] immutableSet = ImmutableSet.of(4,5,6); System.out.println(immutableSet);//[4, 5, 6] ImmutableMap<String, Integer> immutableMap = ImmutableMap.of("a", 1, "b", 2); System.out.println(immutableMap);//{a=1, b=2} ImmutableSet<Person> personImmutableSet = ImmutableSet.<Person>builder() .add(new Person("p1")) .add(new Person("p2")) .build(); System.out.println(personImmutableSet);//[Person(name=p1), Person(name=p2)] //拷贝到不可变集合 immutableSet = FluentIterable.of(7,8,9).toSet(); System.out.println(immutableSet);//[7, 8, 9] //拷贝到不可变集合(排序) Comparator<Integer> comparator = (h1, h2) -> h1.compareTo(h2); immutableSet = FluentIterable.of(7, 8, 9, 5, 4, 7, 9).toSortedSet(comparator); System.out.println(immutableSet);//[4, 5, 7, 8, 9] //反转排序 immutableSet = FluentIterable.of(7, 8, 9, 5, 4, 7, 9).toSortedSet(comparator.reversed()); System.out.println(immutableSet);//[9, 8, 7, 5, 4] }
Lists
静态工厂方法
名称 | 方法(根据入参类型不同区分) |
---|---|
ArrayList | basic, with elements, from Iterable, from Iterator, with exact capacity, with expected size |
LinkedList | basic, from Iterable |
>注:
-
basic
: 无参构造器 -
with elements
: E... elements -
from Iterable
: Iterable<? extends E> elements -
from Iterator
: Iterator<? extends E> elements -
with exact capacity
: int initialArraySize -
with expected size
: int estimatedSize
除了静态工厂方法和函数式编程方法,Lists为List类型的对象提供了若干工具方法。
方法 | 描述 |
---|---|
partition(List, int) | 把List按指定大小分割 |
reverse(List) | 返回给定List的反转视图。注: 如果List是不可变的,考虑改用ImmutableList.reverse() |
/** * Lists 方法测试 */ @Test public void testLists(){ //反转排序 List list = Ints.asList(1, 2, 3, 4, 5); System.out.println(Lists.reverse(list));//[5, 4, 3, 2, 1] //指定大小分割 List<List> parts = Lists.partition(list, 2); System.out.println(parts);//[[1, 2], [3, 4], [5]] List<Integer> iList = Lists.newArrayList(); List<Integer> iList2 = Lists.newArrayList(1, 2, 3); List<Integer> iList3 = Lists.newArrayList(iList2.iterator()); List<Integer> iList4 = Lists.newArrayList(Iterables.concat(iList2)); List<Integer> iList5 = Lists.newArrayListWithCapacity(1); List<Integer> iList6 = Lists.newArrayListWithExpectedSize(1); System.out.println(iList);//[] System.out.println(iList2);//[1, 2, 3] System.out.println(iList3);//[1, 2, 3] System.out.println(iList4);//[1, 2, 3] System.out.println(iList5);//[] System.out.println(iList6);//[] iList5.addAll(iList2); System.out.println(iList5);//[1, 2, 3] iList6.addAll(iList2); System.out.println(iList6);//[1, 2, 3] }
5.缓存
缓存在应用中是必不可少的,经常用的如redis、memcache以及内存缓存等。Guava是Google出的一个工具包,它里面的cache即是对本地内存缓存的一种实现,支持多种缓存过期策略。
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好。
Guava Cache 适用于:
-
你愿意消耗一些内存空间来提升速度。
-
你预料到某些键会被查询一次以上。
-
缓存中存放的数据总量不会超出内存容量。(Guava Cache 是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试 Memcached 这类工具)
如果你的场景符合上述的每一条,Guava Cache 就适合你。
Guava cache的缓存加载方式有两种:
-
CacheLoader
-
Callable callback
这两种创建方式有什么异同呢?
两种方式同样按照获取缓存-如果没有-则计算(get-if-absent-compute)的缓存规则对缓存数据进行的处理的。
不同之处在于:在缓存中没有得到value的时候,CacheLoader会定义一个比较宽泛的、统一的统一的根据key值load value的方法,而Callablee的方式较为灵活,允许你在get的时候指定。
5.1 创建(加载)cache
两种方法 CacheLoader和Callable ,直接上代码 :
定义CacheLoader对象,重载load、loadAll方法,
由于 CacheLoader 可能抛出异常,LoadingCache.get(K)也声明为抛出 ExecutionException 异常。如果你定义的 CacheLoader 没有声明任何检查型异常,则可以通过 getUnchecked(K)查找缓存;
public void testCacheLoader() throws ExecutionException{ LoadingCache<String,String> cahceBuilder = CacheBuilder .newBuilder() .build(new CacheLoader<String, String>() { // 在load方法中定义value的加载方法; // 这个方法要么返回已经缓存的值,要么使用 CacheLoader 向缓存原子地加载新值 @Override public String load(String key) throws Exception { String strProValue="hello " + key + "!"; return strProValue; } // 默认情况下,对每个不在缓存中的键,getAll 方法会单独调用 CacheLoader.load 来加载缓存项。如果批量的加载比多个单独加载更高效,你可以重载 CacheLoader.loadAll 来利用这一点 @Override public Map<String, String> loadAll(Iterable<? extends String> keys) throws Exception { return super.loadAll(keys); } }); logger.info("jerry value:"+cahceBuilder.apply("jerry")); logger.info(("jerry value:"+cahceBuilder.get("jerry"))); logger.info(("peida value:"+cahceBuilder.get("peida"))); logger.info(("peida value:"+cahceBuilder.apply("peida"))); logger.info(("lisa value:"+cahceBuilder.apply("lisa"))); cahceBuilder.put("harry", "ssdded"); logger.info(("harry value:"+cahceBuilder.get("harry"))); }
Callable, 实现了模式"如果有缓存则返回;否则运算、缓存、然后返回"。
这个方法返回缓存中相应的值,或者用给定的 Callable 运算并把结果加入到缓存中。在整个加载方法完成前,缓存项相关的可观察状态都不会更改。
public void testCallable() throws ExecutionException{ Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build(); // 获取某个key时,在Cache.get中单独为其指定load方法 String resultVal = cache.get("jerry", new Callable<String>() { public String call() { String strProValue="hello "+"jerry"+"!"; return strProValue; } }); logger.info("jerry value : " + resultVal); // 获取某个key时,在Cache.get中单独为其指定load方法 resultVal = cache.get("peida", new Callable<String>() { public String call() { String strProValue="hello "+"peida"+"!"; return strProValue; } }); logger.info("peida value : " + resultVal); }
5.2 添加 ,插入key
get : 要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值;
getUnchecked:CacheLoader 会抛异常,定义的CacheLoader没有声明任何检查型异常,则可以 getUnchecked 查找缓存;反之不能;
getAll :方法用来执行批量查询;
put : 向缓存显式插入值,Cache.asMap()也能修改值,但不具原子性;
getIfPresent :该方法只是简单的把Guava Cache当作Map的替代品,不执行load方法;
5.3 清除 key
最残酷的现实就是我们并没有足够的内存去缓存我们想缓存的一切数据。你必须决定: 在什么时候不应该保存一个缓存条目?
Guava 提供了三种基本类型的对象回收策略:
-
基于大小的回收,
-
基于时间的回收,
-
基于引用的回收。
回收的参数(这些参数在缓存回收时发挥作用):
-
大小的设置:CacheBuilder.maximumSize(long) CacheBuilder.weigher(Weigher) CacheBuilder.maxumumWeigher(long)
-
时间:expireAfterAccess(long, TimeUnit) expireAfterWrite(long, TimeUnit)
-
引用:CacheBuilder.weakKeys() CacheBuilder.weakValues() CacheBuilder.softValues()
-
明确的删除:invalidate(key) invalidateAll(keys) invalidateAll()
-
删除监听器:CacheBuilder.removalListener(RemovalListener)
3.1基于大小(容量)的回收
如果你的cache缓存不能超过一定大小的话,只需要使用CacheBuilder.maximumSize(long)方法即可。那么缓存cache将会试图回收那些不经常用到的条目。Warning: 缓存会在到达上限之前把某些条目的内存回收掉。
LoadingCache<String,String> cahceBuilder = CacheBuilder .newBuilder() .maximumSize(10000) .build(new CacheLoader<String, String>() { ... ... });
除此之外,如果不同的条目具有不同权重 --例如,如果你的不同缓存值占用不同的内存空间的话,你可以使用CacheBuilder.weigher(Weigher)方法来指定一个权重,并且可以通过CacheBuilder.maximumWeight(long) 设置最大权重值。在这里补充一个说明,和maximumSize要求的一样,权重值要在条目创建的时候被计算出来的并且在那之后都是不可变的。
3.2 基于时间的回收
CacheBuilder 提供两种定时回收的方法:
-
expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
-
expireAfterWrite(long, TimeUnit)::缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。 如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。
3.3基于引用的回收
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache 可以把缓存设置为允许垃圾回收:
-
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是 equals 比较键。
-
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是 equals 比较值。
-
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是 equals 比较值。
3.4手动回收
guava cache 自带 清除机制,但仍旧可以手动清除:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
5.4 清理什么时候发生?
使用 CacheBuilder 构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。 这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样 CacheBuilder 就不可用了。 相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用 Cache.cleanUp()。ScheduledExecutorService 可以帮助你很好地实现这样的定时调度。
刷新
刷新和回收不太一样。正如 LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。 如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃。
-
重载 CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。
-
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以为缓存增加自动定时刷新功能。和 expireAfterWrite相反,refreshAfterWrite 通过定时刷新可以让缓存项保持可用,但请注意:缓存项只有在被检索时才会真正刷新(如果 CacheLoader.refresh 实现为异步,那么检索不会被刷新拖慢)。因此,如果你在缓存上同时声明 expireAfterWrite 和 refreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
5.5 监听
在guava cache中移除key可以设置相应得监听操作,通过 CacheBuilder.removalListener(RemovalListener),你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,RemovalListener 会获取移除通知RemovalNotification,其中包含移除原因[RemovalCause]、键和值。
下面是一个数据库连接缓存的例子:
public void testRemovalListener(){ class DatabaseConnection{ // 实现省略 DatabaseConnection(String conn){} void close(){} } // 创建loader CacheLoader<String, DatabaseConnection> loader = new CacheLoader<String, DatabaseConnection> () { public DatabaseConnection load(String key) throws Exception { return new DatabaseConnection(key); } }; // 创建移除监听器 RemovalListener<String, DatabaseConnection> removalListener = new RemovalListener<String, DatabaseConnection>() { public void onRemoval(RemovalNotification<String, DatabaseConnection> notification) { DatabaseConnection conn = notification.getValue(); conn.close(); // tear down properly } }; Cache<String, DatabaseConnection> cache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.MINUTES) .removalListener(removalListener) .build(loader); }
5.6 统计
guava cache还有一些其他特性,比如weight 按权重回收资源,统计等,这里列出统计。CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后Cache.stats()方法返回如下统计信息:
-
hitRate():缓存命中率;
-
hitMiss(): 缓存失误率;
-
loadcount() ; 加载次数;
-
averageLoadPenalty():加载新值的平均时间,单位为纳秒;
-
evictionCount():缓存项被回收的总数,不包括显式清除。
唯一值得注意的一点是:当通过asmap()方法查询key时,stat项是不作任何变化的,修改值时会有影响。此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能监控时可以依据的重要指标。
参考文献: