从Guava Cache到ConcurrentHashMap到HashMap
1、简介
Guava是google的一款开源java工具集库,其中许多功能甚是好用。其中的cache缓存功能,如果抛开业界的分布式缓存软件不提,guava cache是非常优秀的缓存工具。下面来看看美团技术团队对它的评价。
总体来看,Guava Cache在高并发场景支持和线程安全上都有相应的改进策略,使用Reference引用命令,提升高并发下的数据……访问速度并保持了GC的可回收,有效节省空间;同时,write链和access链的设计,能更灵活、高效的实现多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等;编程式的build生成器管理,让使用者有更多的自由度,能够根据不同场景设置合适的模式。
2、Guava Cache
guava cache实际是针对单机内存的缓存工具,其实正如HashMap、ConcurrentHashMap一样,他们都是将缓存加载到内存中。但是HashMap和ConCurrentHashMap只能通过主动删除来进行缓存回收。而guava Cache则可以支持通过容量、时间和引用的缓存回收机制。
2.1 数据淘汰策略
常用的缓存淘汰策略有以下几种:
2.2.1 FIFO:First In First Out
1、核心思想:简单的将数据塞入队列中,push --> pop 。
2、优点:当队列中的容量超过阈值时,先进的先出,直接将最先进入队列的元素挤出队列。
3、缺点:没有考虑缓存命中率的问题,使用较少。
2.2.2 LRU:Least Recently Used
1、核心思想:按照缓存命中的时间排序,不考虑单个数据命中的次数。超过阈值时,淘汰最久没用的数据。
2、优点:适用于热点数据的存放,高频访问的保留。
3、缺点:即使某个数据命中率很高,但最近该数据没有使用,依然会被淘汰。周期性高频访问数据会被删掉。
2.2.3 LFU:Least Frequently Used
1、核心思想:淘汰最不常用的数据,对数据的命中次数加权重。
2、优点:LFU要优于LRU,避免周期性高频访问数据被删除。
3、缺点:LFU存在历史数据影响将来数据的“缓存污染”效用。
guava cache采用的是LRU策略。
3、ConcurrentHashMap 与 HashMap
guava cache的底层其实是对ConcurrentHashMap的优化重写,这边文章从guava localCache到ConcurrentHashMap再到HashMap的源码都好好梳理一遍。
3.0.1 ArrayList
- 数组的扩容为何是通过拷贝来完成的?
- 1、数组的内存地址需要是连续的,如果一味的在原有基础上扩容,则需要更大且连续的内存空间来完成,显然无法满足。
- 2、如果强行延展连续的内容空间来满足扩容的需要,将耗费更多的资源来完成其他内存空间的挪动。

3.0.2 LinkedList

3.1 JDK1.7 HashMap

3.1.1 初始化HashMap
new HashMap(),调用构造方法,将初始化HashMap数组长度(JDK1.7的HashMap由数组加链表组成,默认值1 << 4,二进制左移4位=16)和负载因子(影响扩容、get读取数据效率的一个参数,默认0.75即元素超过75%时就要考虑扩容啦)

3.1.2 put方法

HashMap.get()方法
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
hashMap.get(key)方法,先计算key的hash值,然后计算对应下标,在数组中找到对应位置,遍历整个链表找到所在key的value
我们都知道
1、数组查找快,插值慢(因为数组在物理内存中地址连续,对应下标容易找到具体位置,但是插值慢是因为需要将插值后续的元素挨个后移,所以插值慢)
2、而链表插值快,查找慢(插值只需要将next引用的地址修改即可,查找则需要遍历整个表数据)
JDK1.7 hashMap为了解决链表过长所做的努力有:
2.1 在扩容时,同一数组下的链表,重新计算下标,打乱原先过长的链表
2.2 看经典案例2所述,多次二进制右移,保证数据散列度
3.1.3 扩容
1.7HashMap扩容的源码比较简单,源码如下面代码,示意见下图。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e

本文详细探讨了GuavaCache的数据淘汰策略,包括FIFO、LRU和LFU,并介绍了GuavaCache如何利用ConcurrentHashMap实现线程安全的缓存。同时,对比分析了JDK1.7和1.8中HashMap与ConcurrentHashMap的实现细节,如初始化、扩容和put方法。文章还讨论了DougLea的经典写法,解析了提高散列度的技巧,并解释了为何单线程下会抛出并发修改异常。最后,对比了JDK1.7和1.8中ConcurrentHashMap的线程安全性差异。
最低0.47元/天 解锁文章
1327

被折叠的 条评论
为什么被折叠?



