一 Java7/8 中的 HashMap 和 ConcurrentHashMap 源码分析
http://www.importnew.com/28263.html?replytocom=643805#respond (强烈推荐 )
Java7 HashMap
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。
链表中存储的是Entry,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍,默认16。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor,当插入的个数大于这个数时便会扩容。
put 过程分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
计算数组下标方法: hash & (table.length-
1
);
get 过程分析
- 根据 key 计算 hash 值。
- 找到相应的数组下标:hash & (length – 1)。
- 遍历该数组位置处的链表,直到找到相等(==或equals)的 key。
1 2 3 4 5 6 7 8 9 |
|
getEntry(key):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Java7 ConcurrentHashMap
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment。
concurrencyLevel:并行级别、并发数、Segment 数
initialCapacity:初始容量,整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的
put 过程分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
根据 hash 值很快就能找到相应的 Segment,之后就是 Segment 内部的 put 操作了。
Segment 内部是由 数组+链表 组成的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
|
get 过程分析
- 计算 hash 值,找到 segment 数组中的具体位置。
- 槽中也是一个数组,根据 hash 找到数组中具体的位置
- 到这里是链表了,顺着链表进行查找即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Java8 HashMap
由 数组+链表+红黑树 组成。当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别.
put 过程分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
和 Java7 稍微有点不一样的地方就是,Java7 是先扩容后插入新值的,Java8 先插值再扩容,不过这个不重要。
get 过程分析
- 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
- 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
- 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
- 遍历链表,直到找到相等(==或equals)的 key
1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Java8 ConcurrentHashMap
结构与Java8 HashMap 基本一样。采用CAS和synchronized来保证并发安全,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发
put 过程分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
get 过程分析
- 计算 hash 值
- 根据 hash 值找到数组对应位置: (n – 1) & h
- 根据该位置处结点性质进行相应查找
- 如果该位置为 null,那么直接返回 null 就可以了
- 如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
- 如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
- 如果以上 3 条都不满足,那就是链表,进行遍历比对即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
二 关于HashMap死循环
在扩容时发生,两线程对同一HashMap进行扩容,导致产生循环链表。
https://coolshell.cn/articles/9606.html
(看图片就能理解)
三 HashMap里面的数组size必须是2的次幂?
当用hash值进行数组下标匹配的时候,为了使每个链表分配均匀(防止有的链表存储数据过多,有的过少),应当使数组的size为2的次幂。
int index(String key) { //求数组下标的算法
int hash = hash(key.hashCode());
return hash & (LOCK_NUM - 1);
}
当 LOCK_NUM 为2的次幂时,hash值 &LOCK_NUM产生的结果分布均匀。
原文:https://nanguocoffee.iteye.com/blog/907824
四 HashMap为何从头插入改为尾插入
JDK1.7的头插法 JDK1.8的尾插法
HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
总结下HashMap在1.7和1.8之间的变化:
- 1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
- 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
- 1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法。
作者:SevenBlue
链接:https://juejin.im/post/5ba457a25188255c7b168023