HashMap扩展/ConcurrentHashMap/LinkedHashMap

HASHMAP扩展

之前读Effective Java的时候稍微总结过HashMap: http://www.jianshu.com/p/24c10ce29c85
写这篇的原因:

  • 看了小灰的关于HashMap的漫画,就想要再整理一下关于HashMap的一些知识。
  • 2017.11.29,我们的项目里用到了LRUCache,正好LeetCode也有这题,我就去做了下,先后思考了很多种数据结构,最后我想到了能不能用LinkedHashMap这样有序的Map。所以请了解了下LinkedHashMap。

0x00 散列方法和解决HASH冲突的方法

之前考研复习数据结构的时候背过各种名词,也画过、算过各种解决hash冲突的图和公式,但是当时其实并不理解HashMap,毕竟当时根本没什么开发经验。所以现在有点混淆了,就整理下各种名词:

散列的方法

散列的方法,也就是把key的hashcode分配到table的各个index的slot上的方法有:

(1)直接定址法:比如在一个0~100岁的年龄统计表,我们就可以把年龄作为地址。
(2)平方取中法
(3)除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。该方法的关键是选取p。选取的p应使得散列函数值尽可能与关键字的各位相关。p最好为素数。
(4)随机数法
(5)取模法

Java的HashMap里用的就是我们众所周知的&方法,类似(5),但效率更高:

index = HashCode(Key) & (Length - 1)

解决冲突的方法:

就是由于key的hashCode相同或者hashCode散列出来的index相同,需要放在一个bucket里的方法,也就是怎么在桶里排序。

(1) 拉链法
这个印象最深了,拉链法就是Java的HashMap里用到的方法,也就是「头插法」,后加上的元素,放在首位,因为设计者认为后加上的元素被访问的概率更大,所以这样遍历链表效率高。我原本以为这样put是一个O(1)操作,但其实也不是,因为你首先要确定这个bucket里是否有个key的hash,如果有,就要覆盖掉这个hash对应的value。我常常会理解错,认为bucket里存放的是value,这是不对的,bucket里存放的是key的hash,value只是hash的附属品,看下下面的图就明白了:


HashMap扩展/ConcurrentHashMap/LinkedHashMap_第1张图片
image

(2)开放定址法
不知道这个名字代表的啥了,我印象里反正就是把hash先定位到一个slot(那显然就不是链表,而是数组了),然后往左或者往右挪,直到找到一个slot坑位。

0x01 PUT要点

简单说下PUT过程:

对key的hashCode()做hash,然后再计算index;
如果没碰撞直接放到bucket里;
如果碰撞了,以链表的形式存在buckets后;
如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
如果节点已经存在就替换old value(保证key的唯一性)
如果bucket满了(超过load factor*current capacity),就要resize。

注意几个点吧,

  1. hash时用的一般是取模法,发生collision(hash冲突,碰撞)的时候用拉链法之类的解决hash冲突。
  2. hash碰撞导致一个bucket的链表长度大于TREEIFY_THRESHOLD的时候就转换成红黑树,put/get一次的复杂度变为O(logn)
  3. 如果默认的capacity是16,capacity是0.75,那么当12个桶里都有元素的时候,就会自动resize,这样你在一个桶里找东西变得容易了呀。

小灰漫画:什么是HashMap提到的一些细节:

  1. get的时候,后插入的节点会在Entry的前面(头插法)。因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。注意,因为Entry是链表,所以是有序的,这个跟bucket的排列不同。

  2. HashMap初始长度是16(1111B)。长度之所以是2的幂是因为,HashMap的Hash并不是用mod(length -1)完成的,而是用 & (length - 1)完成的,众所周知&效率比%高。而用&的话,当然低位都是1最好了,这样Hash Collision的几率最小。

0x02 高并发下的HashMap产生的循环链表

小灰漫画:高并发下的HashMap

  1. Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
    HashMap.Size >= Capacity * LoadFactor。

  2. Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

这样Resize之后get一个不存在的key的时候就会陷入死循环:


HashMap扩展/ConcurrentHashMap/LinkedHashMap_第2张图片
image.png

0x03 ConcurrentHashMap解决HashMap的线程安全问题

小灰漫画: ConcurrentHashMap

HashTable或者Collections.synchronizedMap都是线程安全的(HashTable与HashMap的另一个区别是HashTable不允许空的key-value)。但是他们都是对整个集合加锁,导致同时间的其他操作阻塞(不能同事put和get了,我们知道HashMap之所以不安全,是因为put的时候rehash的时候产生的循环链表)。

ConCurrentHashMap用了Segment,每个Segment读写自治,Segment间不影响。我们知道HashMap线程不安全是产生在并发写入的时候,那
Case1:不同Segment的并发写入
Case2:同一Segment的一写一读
这两个Case本来就是没问题的;只有在
同一Segment并发put才会上锁:


HashMap扩展/ConcurrentHashMap/LinkedHashMap_第3张图片
image.png

concurrenthashmap中大量运用了volatile。

另外要注意,小灰的几个漫画都是基于Java1.7,在1.8中,上面提到,hash碰撞导致一个bucket的链表长度大于TREEIFY_THRESHOLD的时候就转换成红黑树,put/get一次的复杂度变为O(logn)。

0x04 LinkedHashMap与LruCache

首先,LinkedHashMap为什么能实现顺序存储Entry,我们知道HashMap是用了& 2的幂的Hash算法把key的hash hash到table(数组)上去,所以它的bucket(Entry)的顺序是乱的,LinkedHashMap总不能为了保持顺序就不去hash吧,那样就不是HashMap了,Hash本身就是为了解决冲突->快速访问存在的,如果顺序存储,那就成了ArrayList之类的结构了。
想到这儿,我又开了脑洞,为什么HashMap做不到像ArrayList那样一个bucket只存一个数据?
为什么ArrayList的查找只能是遍历,不能像Map一样是O(1)?
每当有这个疑问的时候,就看下这个Hash公式吧:

index = HashCode(Key) & (Length - 1)

  1. 之所以可以准确定位,是因为bucket存放的,是key的hash经过与操作后得到的index,存是这么存,取也根据这个取,所以是O(1);
  2. 之所以不能一个bucket存放一个数据,是因为Hash算法总会发生冲突,有两种可能
    (1) 不同的key,hashCode相同
    (2)不同的hashCode,与操作得到的index相同,比如默认Capacity16,1011010101110 1001 & 1111得到index = 1001B = 9,只是判断最后4bit的,自然会有冲突。

LruCache的具体实现,移步LeetCode - 146. LRU Cache。

你可能感兴趣的:(HashMap扩展/ConcurrentHashMap/LinkedHashMap)