HashMap与ConcurrentHashMap与LoadingCache(Guava)之间的区别

4

在 Spring Boot 应用程序中,要本地缓存一些数据,就读/写操作而言,哪种技术更好? HashMap vs ConcurrentHashMap vs LoadingCache(Guava library) 我尝试了对每个选项进行的写入和读取操作,其中 HashMap 最快,而 LoadingCache 最慢,那么我们为什么要使用 LoadingCache?它有什么特殊用途吗?

编辑: 该应用程序是多线程的。 并且可以牺牲缓存的最大大小、过期时间等功能。 此外,主要目的是提高读取速度。


你是如何比较它们的性能的?LoadingCache比HashMap更强大,提供更多的功能。 - luk2302
LoadingCache提供了比通常的HashMap缓存更多的功能:您可以指定最大条目数,可以指定条目过期时间后它们将被“重新加载”等等。 - user6073886
1
你没有提及你的应用程序是单线程还是多线程的。 - Stephen C
有几个因素需要考虑,就像其他人已经提到的那样,例如:1)需要线程安全吗?预期的并发量是多少? 2)缓存应该包含多少值,并且是否有类似LRU的清理策略? 3)你想自己处理缓存未命中吗? - Thomas
也许更新问题,以使与“Spring Boot”的关系清晰。 - cruftex
2个回答

3

关于性能问题,它取决于你的数据大小和读取之间修改的比率。以下是一个建议:

静态数据:如果你的数据是静态的,在构造函数中初始化一个只读映射,代码如下:

final Map map;
MyClass(Map inputMap) {
  map = Map.copyOf(inputMap);
}
Object get(Object key) {
  return map.get(key);
}

罕见的修改: 如果您有罕见的修改并且数据不太大:

volatile Map map = Map.of();
Object synchronized put(Object key, Object value) {
  Map mutable = new HashMap(map);
  mutable.put(key, value);
  map = Map.copyOf(mutable);
}
Object get(Object key) {
  return map.get(key);
}
Map.copyOf从Java 9开始提供。它创建了一个不可变的哈希表,该哈希表使用开放寻址方案,不同于HashMap。这将比HashMap更快。您还可以在多线程环境中使用上述方案的HashMap,因为它在创建后不会被修改。
使用同步锁synchronized可以确保在多个线程同时使用put时不使用更新。需要使用volatile以确保更新在其他线程中可见。

主要目的是增加读取速度。

因此,上述解决方案将提供最佳的读取速度,但牺牲了更新速度。

大量数据和/或大量修改:请使用ConcurrentHashMap

即使有轻微的性能优势,我建议使用ConcurrentHashMap,原因如下:
  • 它更少出错,已经证明ConcurrentHashMap是有效的。您会编写具有多个线程验证代码正确工作的单元测试吗?
  • 代码更少。bug更少
  • 代码更少。对您的同事开发人员来说更不会混淆
  • 使用模式可能随时间而变化,您的自制“性能改进”将变成“性能问题”。

脚注:

使用缓存

缓存和LoadingCache:Guava LoadingCache用于与CacheLoader一起使用。缓存加载器有助于使缓存自动填充缓存和/或进行刷新。同时,Guava缓存已过时,我建议查看Caffine或cache2k,以获取在Java堆中工作的缓存解决方案。

缓存始终具有额外的开销,因为它需要执行一些簿记以了解当前访问哪些条目。在cache2k中,此开销最小,至少根据我的(免责声明…)基准测试

Spring Boot

当与Spring缓存抽象一起使用时,例如使用@Cacheable,则实现之间不会有很大的性能差异,因为缓存抽象也具有非常重要的开销。

Spring中的简单cache实现仅适用于测试和原型设计。我建议尽快使用真正的缓存实现并设置合理的资源限制。

分析和优化整个应用程序

您所做的每个优化都具有权衡,因此您应该始终查看整个应用程序并将“优化”与最简单或最常见的解决方案进行比较。


2
如果您的应用程序是多线程的,并且缓存由线程共享,那么HashMap不是一个选项。它不是线程安全的,如果您使用锁等进行保护,则锁定很可能成为并发瓶颈。
如果您的应用程序需要缓存为LRU或具有某些其他“智能”策略来决定何时清除缓存,则ConcurrentHashMap没有提供很好的方法来实现。如果您希望您的缓存对内存压力敏感(例如使用弱引用键),则ConcurrentHashMap也无法实现。
这就是Guava Cache类的用处所在。阅读this以了解它们提供的功能。
底线是,虽然在单线程基准测试中HashMap最具性能,但它可能无法提供您所需的所有功能。性能并非一切。
我的建议是先使代码正常工作...然后再考虑优化方法。一个超快速缓存实现,偶尔会出现故障或填满内存,这不是一个好主意。而且缓存读写性能很可能并不关键。

感谢详细的解释。Guava缓存是否像HashMap和ConcurrentHashMap一样使用堆内存? - Java Programmer
据我所知,不。但这不也是一个“过早优化”的问题吗? - Stephen C
请注意,离堆缓存意味着数据以序列化形式存储在缓存中。反序列化开销相对于(例如)HashMap.getConcurrentHashMap.get会增加几个数量级。Guava 也是如此。 - Stephen C
Guava缓存纯粹使用堆。 - cruftex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接