HashMap--Java8和Java7

特点

  1. 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  2. 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  3. 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  4. 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  5. 计算index方法:index = hash & (tab.length – 1)

 

数据结构

JDK7中HashMap采用的是位桶+链表的方式

JDK8中采用的是位桶+链表/红黑树的方式

当某个位桶的链表的长度超过8的时候,这个链表就将转换成红黑树。因为引入了树,所以其他操作也更复杂了,比如put方法以前只要通过hash计算下标位置,判断该位置有没有元素,如果有就往下遍历,如果存在相同的key就替换value,如果不存在就添加。但是到了8以后,就要判断是链表还是树,如果是链表,插入后还要判断要不要转化成树。不过这些操作都是常量级别的,复杂度还是O(1)的,但是对整体性能提升非常大。链表转换红黑树在treeify方法里实现,给树插入节点在puttreeval方法,修正红黑树是balanceInsertion方法。

 

resize扩容方法

jdk7里hashmap resize时对每个位桶的链表的处理方式(transfer方法),整体过程就是先新建两倍的新数组,然后遍历旧数组的每一个entry,直接重新计算新的索引位置然后头插法往拉链里填坑

jdk8的代码里是把链表上的键值对按hash值分成lo和hi两串,lo串的新索引位置与原先相同[原先位置j],hi串的新索引位置为[原先位置j+oldCap]。这么做的原因是,我们使用的是2次幂的扩展(newCap是oldCap的两倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置,也就是原索引+oldCap。为啥

 

Node

Hashmap的底层实现是使用一个entry数组存储,默认初始大小16,不过jdk8换了名字叫node,可能是因为引入了树,叫node更合适。

 

 

JDK7中HashMap扩容机制的死循环问题

(put方法里,有个addEntry()新建enty的方法,在这个方法里,调用了resize()扩容方法)

//扩容方法

void resize(int newCapacity) {

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30

threshold = Integer.MAX_VALUE;

return;

}

Entry[] newTable = new Entry[newCapacity];//新建一个新表

boolean oldAltHashing = useAltHashing;

useAltHashing |= sun.misc.VM.isBooted() &&

(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash

transfer(newTable, rehash);//完成旧表到新表的转移

table = newTable;

threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}

 

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;//引用next

if (rehash) {

e.hash = null == e.key ? 0 : hash(e.key);

}

int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。

e.next = newTable[i];//头插法插入新表中

newTable[i] = e;

e = next;

}

}

 

由于缺乏同步机制,当多个线程同时resize的时候,某个线程t所持有的引用next,可能已经被转移到了新桶数组中,那么最后该线程t实际上在对新的桶数组进行transfer操作。如果有更多的线程出现这种情况,那很可能出现大量线程都在对新桶数组进行transfer,那么就会出现多个线程对同一链表无限进行链表反转的操作,极易造成死循环,数据丢失等等

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值