guava
Guava 项目是 Google 公司开源的 Java 核心库,它主要是包含一些在 Java 开发中经常使用到的功能,如数据校验、不可变集合、计数集合,集合增强操作、I/O、缓存、字符串操作等。并且 Guava 广泛用于 Google 内部的 Java 项目中,也被其他公司广泛使用,甚至在新版 JDK 中直接引入了 Guava 中的优秀类库,所以质量毋庸置疑。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
集合
不可变集合
不可变对象有很多优点,包括:
- 不受信任的库可以安全使用。
- 线程安全:可以被许多线程使用,没有竞争条件的风险。
- 不需要支持突变,并且可以通过该假设节省时间和空间。所有不可变集合实现都比它们的可变兄弟更节省内存。
- 可以用作常数,期望它保持不变。
- 不支持 null 值
- 中途也是不可变的
制作对象的不可变副本是一种很好的防御性编程技术。Guava 为每种标准类型提供简单易用的不可变版本 Collection,包括 Guava 自己的Collection变体。
创建不可变集合的三种方法
// jdk 提供的不可变集合,在语法层面没有提供类似 c# 的 readonly 这样的关键字,而是通过新增加集合类型来实现
// JDK 提供了Collections.unmodifiableXXX方法,但在我们看来,这些可以是
// 笨拙且冗长;在你想制作防御性副本的任何地方使用都不愉快
// 不安全:只有在没有人持有对原始集合的引用时,返回的集合才是真正不可变的
// 低效:数据结构仍然具有可变集合的所有开销,包括并发修改检查、哈希表中的额外空间等。
{
Set<String> set = new HashSet<>();
set.add("a");
set.add("a");
set.add("b");
// jdk 的不可变集合支持 null
set.add(null);
var immutableSet= Collections.unmodifiableSet(set);
//不能被修改,修改就报错 UnsupportedOperationException
//immutableSet.add("c");
//对原集合的修改,不可变集合会受影响,是引用
set.add("c");
immutableSet.forEach(out::println); // a b c
}
// 方法1 of
//不可变集合里存在重复元素也不会报错
{
Set<String> immutableSet = ImmutableSet.of("a", "a", "b");
//不能被修改,修改就报错 UnsupportedOperationException
//immutableSet.add("a");
immutableSet.forEach(out::println); // a b
}
//方法2 builder 构造器模式
{
var build=ImmutableSet.<String>builder();
build.add("a");
build.add("a");
build.add("b");
build.build().forEach(out::println);// a b
}
// 方法3 拷贝来自其他集合
{
var list =new ArrayList<String>();
list.add("a");
list.add("a");
list.add("b");
//深拷贝
var immutableSet=ImmutableSet.copyOf(list);
list.add("c");
//对原集合的修改,不可变集合不会受影响
immutableSet.forEach(out::println);
}
不可变集合除了上面演示的 set 之外,还有很多不可变集合,下面是 Guava 中不可变集合和其他集合的对应关系
可变集合接口 | 属于JDK还是Guava | 不可变版本 |
---|---|---|
Collection | JDK | ImmutableCollection |
List | JDK | ImmutableList |
Set | JDK | ImmutableSet |
SortedSet,NavigableSet | JDK | ImmutableSortedSet |
Map | JDK | ImmutableMap |
SortedMap | JDK | ImmutableSortedMap |
Multiset | Guava | ImmutableMultiset |
SortedMultiset | Guava | ImmutableSortedMultiset |
Multimap | Guava | ImmutableMultimap |
ListMultimap | Guava | ImmutableListMultimap |
SetMultimap | Guava | ImmutableSetMultimap |
BiMap | Guava | ImmutableBiMap |
ClassToInstanceMap | Guava | ImmutableClassToInstanceMap |
Table | Guava | ImmutableTable |
guava 扩展了许多独有的集合便利性非常好
集合分组
Multimap的特点其实就是可以包含有几个重复Key的value,可以put进入多个不同value但是相同的key,但是又不会覆盖前面的内容
map
可变集合的分组,非常方便可以进行分组后获取数据
{
ListMultimap<String,Integer> mapLIst= ArrayListMultimap.create();
mapLIst.put("a",1);
mapLIst.put("a",2);
mapLIst.put("b",3);
mapLIst.put("b",4);
mapLIst.forEach((key,value)->{
out.println("key:"+key+" , value:"+value);
});
// key:a , value:1
// key:a , value:2
// key:b , value:3
// key:b , value:4
var list= mapLIst.get("a");
list.forEach(out::println);
// 1
// 2
}
不可变集合的分组,非常方便可以进行分组后获取数据
{
var multimapList=ImmutableListMultimap.<String,Integer>of(
"a",1,
"a",2,
"b",3,
"b",4);
multimapList.forEach((key,value)->{
out.println("key:"+key+" , value:"+value);
});
// key:a , value:1
// key:a , value:2
// key:b , value:3
// key:b , value:4
var list= multimapList.get("a");
list.forEach(out::println); // 1 2
}
table
Table<R,C,V> table = HashBasedTable.create();,由泛型可以看出,table由双主键R(行),C(列)共同决定,V是存储值 新增数据:table.put(R,C,V) 获取数据:V v = table.get(R,C) 遍历数据: Set
set = table.rowKeySet(); Set set = table.columnKeySet();
用 原生 steam 更方便
// 神奇的表格
{
@Data
@AllArgsConstructor
class TestScore {
private String studentName;
private String subject;
/**
* 年级
*/
private String classGrade;
private Double score;
/**
* 学期
*/
private String semester;
}
TestScore t1 = new TestScore("张三", "语文", "三年二班", 80D, "2021上");
TestScore t2 = new TestScore("张三", "数学", "三年二班", 80D, "2021上");
TestScore t3 = new TestScore("李四", "语文", "三年二班", 100D, "2021上");
TestScore t4 = new TestScore("李四", "数学", "三年二班", 90D, "2021上");
var testScoreList = Lists.newArrayList(t1, t2, t3, t4);
// 统计每个学生的总分
{
//模拟 excel 表格 通过行索引和列索引定位唯一的单元格
var table = HashBasedTable.<String, String, Double>create();
testScoreList.forEach(p -> {
// 行 列 单个格的值
table.put(p.getStudentName(), p.getSubject(), p.getScore());
});
//按行统计
var rows = table.rowMap();
var newMap = Maps.<String, Double>newHashMap();
rows.forEach((p, q) -> {
var count = q.values().stream().mapToDouble(l -> l).sum();
newMap.put(p, count);
});
newMap.forEach((p, q) -> {
out.println(p + ":" + q);
//李四:190.0
//张三:160.0
});
}
// 统计每个学生的总分
//不使用框架实现,通过 stream 实现,代码量差不多
{
var groups = testScoreList.stream().collect(Collectors.groupingBy(p -> p.getStudentName()));
var newMap = new HashMap<String, Double>();
groups.forEach((p, q) -> {
var count = q.stream().mapToDouble(l -> l.score).sum();
newMap.put(p, count);
});
newMap.forEach((p, q) -> {
out.println(p + ":" + q);
//李四:190.0
//张三:160.0
});
}
//计算科目总分
{
//模拟 excel 表格 通过行索引和列索引定位唯一的单元格
var table = HashBasedTable.<String, String, Double>create();
testScoreList.forEach(p -> {
// 行 列 单个格的值
table.put(p.getStudentName(), p.getSubject(), p.getScore());
});
//按列统计
var cols = table.columnMap();
var newMap = Maps.<String, Double>newHashMap();
cols.forEach((p, q) -> {
var count = q.values().stream().mapToDouble(l -> l).sum();
newMap.put(p, count);
});
newMap.forEach((p, q) -> {
out.println(p + ":" + q);
});
}
// 统计每个学科的总分
//不使用框架实现,通过 stream 实现,代码量差不多
{
var groups = testScoreList.stream().collect(Collectors.groupingBy(p -> p.getSubject()));
var newMap = new HashMap<String, Double>();
groups.forEach((p, q) -> {
var count = q.stream().mapToDouble(l -> l.score).sum();
newMap.put(p, count);
});
newMap.forEach((p, q) -> {
out.println(p + ":" + q);
});
}
// 不使用框架实现,通过 stream 实现,代码量可以更少
{
var groups = testScoreList.stream().collect(Collectors.groupingBy(
p -> p.getSubject(),Collectors.summingDouble(p->p.score)));
groups.forEach((p, q) -> {
out.println(p + ":" + q);
});
}
BiMap
用处不大
// BiMap提供双向关系维护,但是业务上需要保证值唯一,如果不唯一,需使用forcePut
// key 和 value 都是唯一的,可以通过 key 找 value,也可以通过 value 找 key
{
var biMap = HashBiMap.<String, Integer>create();
biMap.put("a", 1);
biMap.forcePut("b", 1);
out.println(biMap.get('a'));
out.println(biMap.inverse().get(1));
}
工厂方法
快速的初始化集合
// JDK
{
var unModifyList = List.of(1,2,3);
//要做初始化,只能是这种封锁的方式
var list = new ArrayList<Integer>() {
{
add(1);
add(2);
add(3);
}
};
}
//guava工厂模式
{
var list = Lists.newArrayList(1,2,3);
list.add(4);
list.forEach(System.out::println);
}
集合交集,并集,差集
guava 缓存
缓存分为本地缓存与分布式缓存。本地缓存为了保证线程安全问题,一般使用ConcurrentMap的方式保存在内存之中,而常见的分布式缓存则有Redis,MongoDB等。 缓存在各种用例中都非常有用。例如,当一个值的计算或检索成本很高时,您应该考虑使用缓存,并且您将多次在某个输入上需要它的值
Guava缓存适用于以下情况:
- 愿意花费一些内存来提高速度。
- 使用场景有时会多次查询key。
- 缓存将不需要存储超出RAM容量的数据
- 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
- Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收;
- 监控缓存加载/命中情况
Guava提供了设置并发级别的API,使得缓存支持并发的写入和读取。与ConcurrentHashMap类似,Guava cache的并发也是通过分离锁实现。在通常情况下,推荐将并发级别设置为服务器cpu核心数, Cache有区别于ConcurrentHashMap的使用,就是因为其自带有自动刷新和自动失效的功能,避免我们去自己编写刷新和失效的后台线程程,内存缓存模块,用于将数据缓存到JVM内存中
失效时间,存活时间策略可以单独设置或组合配置
- expireAfterWrite 自条目创建后 经过指定的持续时间或值的最新替换后,使条目过期。--常用,多个线程是线程之间会互相等待
- refreshAfterWrite 是指在创建缓存后,如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,会在后台异步去刷新缓存, 如果新的缓存值还没有load到时,则会先返回旧值 --常用 比 expireAfterWrite 性能好,线程之间不会等待。
- expireAfterAccess 仅在自上次通过读取或写入访问条目 后经过指定持续时间后才使条目过期,只要对条目读取或写入过就进行重新计时,多个线程是线程之间会互相等待
添加,插入key
- get,要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值;
- getUnchecked CacheLoader 会抛异常,定义的CacheLoader没有声明任何检查型异常,则可以 getUnchecked 查找缓存;反之不能;
- getAll,方法用来执行批量查询;
- put,向缓存显式插入值,Cache.asMap()也能修改值,但不具原子性;
- getIfPresent,从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
清除key,guava cache 自带清除机制,但仍旧可以手动清除:
- 个别清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有缓存项:Cache.invalidateAll()
可以通过 asMao() 获取底层的 map
防止缓存击穿
从图上容易看出,每次只有找不到值都会进入load方法,换句话说,只有发生“取值”操作,才会执行load,然而为了防止“缓存穿透”,在多线程的环境下,任何时刻>只允许一个线程操作执行load操作
但在执行load操作这个步骤,expire 与 refresh 的线程机制不同:
- expire 在load 阶段——同步机制:当前线程load未完成,其他线程呈阻塞状态,待当前线程load完成,其他线程均需进行”获得锁--获得值--释放锁“的过程。这种 方法会让性能有一定的损耗。但是所有线程获取的都是最新的值
- refresh 在load阶段——异步机制 :当前线程load未完成,其他线程仍可以取原来的值,等当前线程load完成后,下次某线程再取值时,会判断系统时间间隔是否 超过设定refresh时间,来决定是否设定新值。所以,refresh机制的特点是,设定30分钟刷新,30min后并不一定就是立马就能保证取到新值。 能够想象,只要refresh得时间小于expire时间,就能保证多线程在load取值时不阻塞,也能保证refresh时间到期后,取旧值向新值得平滑过渡,当然,仍旧不能解决取到旧值得问题。
基于过期时间的最简单的缓存
put 和 getIfPresent 配合使用
- put 更新缓存
- getIfPresent 从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null
expireAfterAccess
var cache= CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
.expireAfterAccess(Duration.ofSeconds(1))
//.expireAfterWrite(Duration.ofSeconds(1))
.build();
//写入后重新计算时间
cache.put("a",1);
Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//1
Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//1
Thread.sleep(1000);
//过期了
System.out.println(cache.getIfPresent("a"));
//null
//写入后重新计算时间
cache.put("a",1);
Thread.sleep(900);
//写入后重新计算时间
cache.put("a",2);
Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//2
Thread.sleep(1000);
System.out.println(cache.getIfPresent("a"));
//null
expireAfterWrite
var cache= CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
//.expireAfterAccess(Duration.ofSeconds(1))
.expireAfterWrite(Duration.ofSeconds(1))
.build();
// 自条目创建后,开始计时,后面不会在更新时间
cache.put("a",1);
Thread.sleep(900);
System.out.println(cache.getIfPresent("a"));
// 1
Thread.sleep(900);
System.out.println(cache.getIfPresent("a"));
// null
Thread.sleep(1000);
System.out.println(cache.getIfPresent("a"));
//null
加载 cache
全局缓存 CacheLoader
先取缓存——娶不到再执行load
CacheLoader 和 get 配合使用
- 使用 get 要么返回已经缓存的值,要么使用 CacheLoader 向缓存原子地加载新值
- 任然可以使用 put 更新缓存值
- 在这种情况下使用 getIfPresent 没有意义
{
var cache = CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
.expireAfterAccess(Duration.ofSeconds(1))
//.expireAfterWrite(Duration.ofSeconds(1))
.build(new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String s) {
String tmp = null;
switch (s) {
case "a":
tmp = "a:"+LocalDateTime.now().toLocalTime().toString();
break;
default:
break;
}
return Optional.ofNullable(tmp);
}
}
);
//没有缓存 load
System.out.println(cache.get("a").isPresent()? cache.get("a").get():null);
//a:10:28:19.755542900
Thread.sleep(900);
//缓存有,用缓存
System.out.println(cache.get("a").isPresent()? cache.get("a").get():null);
//a:10:28:19.755542900
Thread.sleep(1000);
//缓存过期,再次 load
System.out.println(cache.get("a").isPresent()? cache.get("a").get():null);
a:10:28:21.699160500
//缓存不存在,load
System.out.println( cache.get("b").isPresent()? cache.get("a").get():null);
}
}
局部 item Callable
Callable 和 get 配合使用
- 使用 get 要么返回已经缓存的值,要么使用 Callable 向缓存原子地加载新值
- 任然可以使用 put 更新缓存值
- 在这种情况下使用 getIfPresent 没有意义
{
var cache = CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
.expireAfterAccess(Duration.ofSeconds(1))
//.expireAfterWrite(Duration.ofSeconds(1))
.build();
//缓存没有值,使用回调生成值
System.out.println(cache.get("a", (Callable<String>) () ->
"a:" + LocalDateTime.now().toLocalTime().toString()));
// a:11:14:19.617927100
Thread.sleep(900);
//这次获取的是缓存值
System.out.println(cache.get("a", (Callable<String>) () ->
"a:" + LocalDateTime.now().toLocalTime().toString()));
//a:11:14:19.617927100
}
refreshAfterWrite
多个线程都去load 的时候,refreshAfterWrite 可以只让一个线程去load,其他线程用旧值代替
{
var cache = CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
//.expireAfterAccess(Duration.ofSeconds(1))
//.expireAfterWrite(Duration.ofSeconds(1))
.refreshAfterWrite(Duration.ofSeconds(1))
.build(new CacheLoader<String, String>() {
@Override
public String load(String s) throws Exception {
String tmp = "";
switch (s) {
case "a":
Thread.sleep(5000);
tmp = "a:" + LocalDateTime.now().toLocalTime().toString();
break;
default:
break;
}
return tmp;
}
});
cache.put("a", "1");
//获取缓存的值
System.out.println(cache.get("a"));
//1
//缓存失效,load
var thread1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +","+ cache.get("a"));
//thread1,a:13:45:53.725228700
}
});
thread1.setName("thread1");
//缓存失效,用旧值替代,因为已经有一个线程已经在load 处于阻塞状态,这个线程直接返回旧值
var thread2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +","+ cache.get("a"));
//thread2,1
}
});
thread2.setName("thread2");
thread1.start();thread2.start();
}
}
弱引用
通过weakKeys和weakValues方法,可以指定Cache只保存“对缓存记录key和value的弱引用”。这样,当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。
{
var cache = CacheBuilder.newBuilder()
// 设置并发级别为cpu核心数,默认为4
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
// 初始记录数,最大记录数
.initialCapacity(10).maximumSize(20)
//.expireAfterAccess(Duration.ofSeconds(1))
.expireAfterWrite(Duration.ofSeconds(1))
//对 key 进行弱引用
.weakValues()
.build();
var value =new Object();
//这里相当于一种绑定关系,把堆里的对象绑定给 key,但是这种绑定是弱引用的关系
cache.put("a", value);
//原来的栈指向新的对象
value=new Object();
//回收弱引用对象
System.gc();
System.out.println(cache.getIfPresent("a"));
//null
}
缓存移除监听器
var cache = CacheBuilder.newBuilder()
//同步移除
.removalListener((notice)->{
var name=Thread.currentThread().getName();
System.out.println(name+",监听到移除的 key:"+notice.getKey());
System.out.println(name+",监听到移除的 value:"+notice.getValue());
})
//异步移除
.removalListener(RemovalListeners.asynchronous(notice->{
var name=Thread.currentThread().getName();
System.out.println(name+",监听到移除的 key:"+notice.getKey());
System.out.println(name+",监听到移除的 value:"+notice.getValue());
}, Executors.newSingleThreadExecutor()))
.build();
cache.invalidate("a");
//缓存移除后,不会显式地立即删除,需要自己清除
cache.cleanUp();
缓存统计
缓存统计功能,需要使用 recordStats() 打开缓存统计功能,没有发现好用 返回如下统计信息:
- hitRate():缓存命中率;
- hitMiss(): 缓存失误率;
- loadcount() ; 加载次数;
- averageLoadPenalty():加载新值的平均时间,单位为纳秒;
- evictionCount():缓存项被回收的总数,不包括显式清除。
计算中间代码的运行时间
{
var stopWatch = Stopwatch.createStarted();
for (int i = 0; i < 10; i++)
Thread.sleep(1000);
var times= stopWatch.elapsed(TimeUnit.SECONDS);
System.out.println(times);
//10
}