HashMap源码分析
JDK7
数组+链表
HashMap<String,String> hashMap= new HashMap<>();//数组+链表
//每一个 都是存的Entry对象
hashmap.put("123","1")//key-->key.hashcode()--> hashcode%数组的长度-->index,
//hash冲突,形成链表,头插法,链表向下移动一格到数组中,方便后续从头开始遍历寻找
//头插法更快。尾插法需要遍历到最后,效率低。
//entry 源码
static class Entry<K,V> implements Map.Entry<K,V>{
final K key;
V value;
Entry<K,V> next;
int hash;
}
put(key,value){
int hashcode=key.hashcode();
int index=hashcode%table.length;
table[index]=new Entry(key,value,null);//第三个参数是next指针。
//第二次插入 发生碰撞 table[index]=new Entry(key,value,table[index]);
}
翻倍扩容
JDK8
1.桶的树形化 treeifyBin()
(1)根据哈希表容量以及元素个数确定是扩容还是树形化;
(2)如果是树形化则遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系;
(3)让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容;
2.put()
①.判断键值对数组table[i]是否为空,否则执行resize()扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否不小于7,不小于7就把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量是否超过阈值,如果超过了就进行扩容;
区别
-
最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
-
jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
-
插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
-
jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
-
扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;
-
jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
-
扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
为什么JDK改用尾插法?
JDK1.7中扩容时,每个元素的rehash之后,都会插入到新数组对应索引的链表头,所以这就导致原链表顺序为A->B->C,扩容之后,rehash之后的链表可能为C->B->A,元素的顺序发生了变化。在并发场景下,扩容时可能会出现循环链表的情况。而JDK1.8从头插入改成尾插入元素的顺序不变,避免出现循环链表的情况。
ConCurrentHashMap源码分析
JDK7
Segement[]数组,Segement[]数组中在存 HashEntry, 一个Segement中对应的entry数量由 初始容量/并发级别算出。
一个Segement最少对应两个Entry.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TPb5fz0o-1626256812421)(C:\Users\10952\AppData\Roaming\Typora\typora-user-images\image-20210714141434486.png)]
当 entry中 链表长度大于 阈值 该Segement中做rehash扩容,与其他Segement无关
Segment的put方法中,会先进行加锁,也就是锁最终加锁的地方在Segment,如果访问的是不同的Segment,就不会有锁的争用,提高了并发性能。
Java7 ConcurrentHashMap基于ReentrantLock实现分段锁
JDK8
jdk1.8中ConcurrentHashMap取消了分段式锁的封装,存储结构和数据操作原理与普通HashMap一样,数组+[链表|树]。其保证线程安全的核心思想是,插入元素时如果没有冲突,也就是说index处为空,则执行CAS插入操作;如果index处已经有元素了,则使用synchronized锁定index处元素,再进行插入操作。
Java8中 ConcurrentHashMap基于分段锁+CAS保证线程安全,分段锁基于synchronized关键字实现;
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//求hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //CAS插入,当位置为空时,不上锁
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//有冲突才加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}