关于JDK1.8 HashMap扩容部分源码分析
今天回顾hashmap源码的时候发现一个很有意思的地方,那就是jdk1.8在hashmap扩容上面的优化。
首先大家可能都知道,1.8比1.7多出了一个红黑树化的操作,当然在扩容的时候也要对红黑树进行重排,然而今天要说的并不是这个,而是针对数组中的链表项的处理优化。
关于hashmap的源码都十分精妙,有时间可以多看看。
首先上1.7的源码:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { //当当前数据长度已经达到最大容量
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的数据转移到新的table中
table = newTable; // 数组重新赋值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); //重新计算阈值
}
很显然,是遍历整个map中的每一个节点(如果是链表就再对其循环),每一个节点新的放置位置=[hash & (newCapacity - 1)]------indexFor方法。
transfer(newTable, rehash); // 将table的数据转移到新的table中
此方法会根据rehash来判断是否重新计算hashCode,然后放置到新的数组中:
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.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
这是transfer将当前数组中各节点e移动到新数组的 i 位置上的核心代码,为了避免调用put方法,它直接取 e.next = newTable[i];
例如:
oldtable[ i ]为:A->B->null
newtable[ j ]为:X->Y->null
移动oldtable[ i ]到newTable[ j ]中,步骤如下:
1. e指向A;
2. e.next指向newTable[ j ]也就是X,所以A->X;
3. newTable[ j ]指向A,所以此时newTable[ j ]为A->X->Y->null
4. e指向B;
类似循环操作1,2,3
最后newTable[ j ]结果为:B->A->X->Y->null,变成了逆序。
并且,并发的时候可能会发生死锁:
假如线程1在刚执行完 Entry<K,V> next = e.next; 后,此时e指向A,next指向B,
然后被B线程抢占,然后B完整执行完扩容后,此时newTable[ j ]为:B->A->X->Y->null,
然后A线程恢复执行,e.next = newTable[i];,此时newTable[ j ]为:B<->A X->Y->null,(已经形成环,并且后面链表丢失)
newTable[i] = e;,此时newTable[ j ]为:A<->B
继续循环,发现已经形成环,没有null了, while(null != e) 永远跳不出循环,所以会形成死锁。
可以看出形成环的主要原因是因为形成了逆序,应该怎么解决呢?
下面是1.8的代码:
final Node<K,V>[] resize() {
//保存旧的 Hash 数组
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//超过最大容量,不再进行扩充
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//容量没有超过最大值,容量变为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//阀值变为原来的两倍
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
//阀值和容量使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//计算新的阀值
float ft = (float)newCap * loadFactor;
//阀值没有超过最大阀值,设置新的阀值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建新的 Hash 表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//遍历旧的 Hash 表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//释放空间
oldTab[j] = null;
//当前节点不是以链表的形式存在
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//红黑树的形式,略过
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//以链表形式存在的节点;
//这一段就是新优化的地方,见下面分析
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//最后一个节点的下一个节点做空
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//最后一个节点的下一个节点做空
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
嗯。。很多,
主要逻辑就是,循环数组内每一个元素
1、是普通节点,直接和1.7一样放置;
2、红黑树,调用 split 修剪方法进行拆分放置(不是本文要点,略);
3、是链表………………
是不是链表那段没看懂?下面举一个例子就好明白了:
假如现在容量为初始容量16,再假如5,21,37,53的hash自己(二进制),
所以在oldTab中的存储位置就都是 hash & (16 - 1)【16-1就是二进制1111,就是取最后四位】,
5 :00000101
21:00010101
37:00100101
53:00110101
四个数与(16-1)相与后都是0101
即原始链为:5--->21--->37--->53---->null
此时进入代码中 do-while 循环,对链表节点进行遍历,判断是留下还是去新的链表:
lo就是扩容后仍然在原地的元素链表
hi就是扩容后下标为 原位置+原容量 的元素链表,从而不需要重新计算hash。
因为扩容后计算存储位置就是 hash & (32 - 1)【取后5位】,但是并不需要再计算一次位置,
此处只需要判断左边新增的那一位(右数第5位)是否为1即可判断此节点是留在原地lo还是移动去高位hi:(e.hash & oldCap) == 0 (oldCap是16也就是10000,相与即取新的那一位)
5 :00000101——————》0留在原地 lo链表
21:00010101——————》1移向高位 hi链表
37:00100101——————》0留在原地 lo链表
53:00110101——————》1移向高位 hi链表
第一轮循环
loHead:5
loTail:5
其他:null
第二轮循环
loHead:5
loTail:5
hiHead:21
hiTail:21
第三轮循环
loHead:5 (5.next = 37)
loTail:37
hiHead:21
hiTail:21
。。。
所以循环结束之后:
loHead:5
loTail:37
hiHead:21
hiTail:53
lo:5--->37---->null
hi:21--->53---->null
退出循环后只需要判断lo,hi是否为空,然后把各自链表头结点直接放到对应位置上即可完成整个链表的移动。
原理是:利用了尾指针Tail,完成了尾部插入,不会造成逆序,所以也不会产生并发死锁的问题。
这种方法对比1.7中算法的优点是:
1、不管怎么样都不需要重新再计算hash;
2、放过去的链表内元素的相对顺序不会改变;
3、不会在并发扩容中发生死锁。
注意,时间复杂度并没有减少
有以上分析同样可以得出hashmap扩容的开销很大,日常开发中应该根据实际需要设定合适的 初始容量 和 负载因子 ,这对提高程序性能有不小帮助。
关于JDK1.8 HashMap扩容部分源码分析的更多相关文章
- JDK1.8 HashMap中put源码分析
一.存储结构 在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...
- 并发-HashMap和HashTable源码分析
HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...
- HashMap原理及源码分析
HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...
- 基于JDK1.8,Java容器源码分析
容器源码分析 如果没有特别说明,以下源码分析基于 JDK 1.8. 在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码. Lis ...
- HashMap(三)之源码分析
通过分析HashMap来学习源码,那么通过此过程我们要带着这几个问题去一起探索 为什么要学习源码 怎么去学习 0.1 为什么要学习源码 这个问题,直接给出结论,学习源码肯定是有好处的,比如: 学习优秀 ...
- HashMap:从源码分析到面试题
1 HashMap简介 HashMap是实现map接口的一个重要实现类,在我们无论是日常还是面试,以及工作中都是一个经常用到角色.它的结构如下: 它的底层是用我们的哈希表和红黑树组成的.所以我们在学习 ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- HashMap主要方法源码分析(JDK1.8)
本篇从HashMap的put.get.remove方法入手,分析源码流程 (不涉及红黑树的具体算法) jkd1.8中HashMap的结构为数组.链表.红黑树的形式 (未转化红黑树时) (转 ...
- ArrayList增加扩容问题 源码分析
public class ArrayList<E>{ private static final int DEFAULT_CAPACITY = 10;//默认的容量是10 private s ...
随机推荐
- python之MySQL学习——简单的增删改查封装
1.增删改查封装类MysqlHelper.py import pymysql as ps class MysqlHelper: def __init__(self, host, user, passw ...
- 巨蟒python全栈开发数据库前端5:JavaScript1
1.js介绍&变量&基础数据类型 2.类型查询&运算符&if判断&for循环 3.while循环&三元运算符 4.函数 5.今日总结 1.js介绍&am ...
- ios 画图总结
0 CGContextRef context = UIGraphicsGetCurrentContext(); 设置上下文1 CGContextMoveToPoint 开始画线2 CGContextA ...
- 初探runtime
1 简介 runtime,也叫它运行时系统.它是用c写的一套API,oc代码底层实现全都依赖它.我们说它是运行时,是相比编译,在程序编译完成之后,一些对象可通过runtime来干一些在编译时看似不可能 ...
- 利用epoll实现异步IO
之前异步IO一直没搞明白,大致的理解就是在一个大的循环中,有两部分:第一部分是监听事件:第二部分是处理事件(通过添加回调函数的方式).就拿网络通信来说,可以先通过调用 select 模块中的 sele ...
- 【我的Android进阶之旅】 Android Studio插件之Jenkins插件介绍
一Jenkins插件功能介绍 1Jenkins任务列表 2切换Jenkins分组 3构建Jenkins任务 4进入构建Jenkins任务的页面 5进入最后一次构建Jenkins任务的页面 6增加Jen ...
- 我的Android进阶之旅------>解决Error:Could not find property 'compile' on org.gradle.api.internal.artifacts.
1错误描述 解决方法 1错误原因 2解决方法 1.错误描述 刚刚,Android Studio突然编译不了了,报了如下错误: Error:Could not find property 'compil ...
- Django继承
Django目前支持两种不同的继承方式,包括抽象基础类和多表继承. 1.抽象基础类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cla ...
- linux c编程:记录锁
记录锁相当于线程同步中读写锁的一种扩展类型,可以用来对有亲缘或无亲缘关系的进程进行文件读与写的同步,通过fcntl函数来执行上锁操作.尽管读写锁也可以通过在共享内存区来进行进程的同步,但是fcntl记 ...
- 收藏:几种开源许可证的区别!——By 阮一峰制作
乌克兰程序员Paul Bagwell,画了一张分析图,下面是阮一峰制作的中文版,非常棒,绝对的好东西,收藏这张图供日后查看: