今天回顾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扩容部分源码分析的更多相关文章

  1. JDK1.8 HashMap中put源码分析

    一.存储结构      在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构.它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置.Hash ...

  2. 并发-HashMap和HashTable源码分析

    HashMap和HashTable源码分析 参考: https://blog.csdn.net/luanlouis/article/details/41576373 http://www.cnblog ...

  3. HashMap原理及源码分析

    HashMap 原理及源码分析 1. 存储结构 HashMap 内部是由 Node 类型的数组实现的.Node 包含着键值对,内部有四个字段,从 next 字段我们可以看出,Node 是一个链表.即数 ...

  4. 基于JDK1.8,Java容器源码分析

    容器源码分析 如果没有特别说明,以下源码分析基于 JDK 1.8. 在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码. Lis ...

  5. HashMap(三)之源码分析

    通过分析HashMap来学习源码,那么通过此过程我们要带着这几个问题去一起探索 为什么要学习源码 怎么去学习 0.1 为什么要学习源码 这个问题,直接给出结论,学习源码肯定是有好处的,比如: 学习优秀 ...

  6. HashMap:从源码分析到面试题

    1 HashMap简介 HashMap是实现map接口的一个重要实现类,在我们无论是日常还是面试,以及工作中都是一个经常用到角色.它的结构如下: 它的底层是用我们的哈希表和红黑树组成的.所以我们在学习 ...

  7. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  8. HashMap主要方法源码分析(JDK1.8)

    本篇从HashMap的put.get.remove方法入手,分析源码流程 (不涉及红黑树的具体算法) jkd1.8中HashMap的结构为数组.链表.红黑树的形式     (未转化红黑树时)   (转 ...

  9. ArrayList增加扩容问题 源码分析

    public class ArrayList<E>{ private static final int DEFAULT_CAPACITY = 10;//默认的容量是10 private s ...

随机推荐

  1. Making training mini-batches

    Here is where we'll make our mini-batches for training. Remember that we want our batches to be mult ...

  2. 【Python之路】第二十四篇--爬虫

    网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本.另外一些不常使用的名字还有蚂蚁.自动索引.模拟程序或者蠕 ...

  3. Java 面向对象之 static 关键字

    static 特点 static 是一个修饰符, 用于修饰成员 static 修饰的成员被所有的对象所共享 static 优先于对象存在, 因为 static 的成员随着类的加载就已经存在了 stat ...

  4. 贪玩ML系列之CIFAR-10调参

    调参方法:网格调参 tf.layers.conv2d()中的padding参数 取值“same”,表示当filter移出边界时,给空位补0继续计算.该方法能够更多的保留图像边缘信息.当图片较小(如CI ...

  5. 我的Android进阶之旅------>Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词的功能

    前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 LRC歌词文件的标签类型 1标识标签 2时间标签 二解析LRC歌词 1读取出歌词文件 2解析得到的歌词内容 1表示每行歌词内 ...

  6. swift 值得学习的项目

    http://www.php100.com/html/it/biancheng/2015/0112/8329.html

  7. mysql 获取随机10条数据

    SELECT * FROM s_user WHERE id>= ((SELECT MAX(id) FROM s_user)-(SELECT MIN(id) FROM s_user)) * RAN ...

  8. tkprof工具详解二

      TKPROF是一个可执行文件,自带在Oracle Server软件中,无需额外的安装. 该工具文件可以用来解析ORACLE的SQL TRACE(10046) 以便生成更可读的内容.  实际上tkp ...

  9. Linux工作管理 jobs、fg、bg、nohup命令

    概述 在Linux 中我们登陆了一个终端,已经在执行一个操作,可以通过一定的操作或命令在不关闭当前操作的情况下执行其他操作. 例如,我在当前终端正在 vi 一个文件,在不停止 vi 的情况下,如果我想 ...

  10. onerror事件

    onerror 事件会在文档或图像加载过程中发生错误时被触发. 案例: <img onerror="this.onerror=null;this.src='/images/common ...