开心一刻

  一天,楼主在路上碰到了一个很久没见的朋友,找了个餐馆,坐下聊了起来

  楼主:在哪上班了 ?

  朋友:火葬场啊

  楼主:在那上班,一个月多少钱啊 ?

  朋友:两万多啊

  楼主(不可思议):多少 ?

  朋友(非常淡定):两万多

  楼主:你们那还要人吗 ?

  朋友:要啊,24小时都要

  楼主:不是,我的意思是你们那还收人吗

  朋友:收,天天都收

  楼主:我是说,我能进去不 ?

  朋友:那200多斤的胖子都能进去,你进不去 ?

  楼主:不是,你是非要把我给炼了是咋地 ? 我能进去不,我能自己进去不 ?

  朋友:那有点悬,都是推进去的

  楼主:我是说,你们那还招工吗

  朋友:招,不分公母,都招

  楼主:老板,买单

  老板:你还没点菜了

  楼主:不点了,再不走就要被炼了

数据结构

  对 HashMap 的底层数据结构,相信大家都有所了解,不同的版本,底层数据结构会有所不同

  1.7 的底层数据结构

/**
* An empty table instance to share when the table is not inflated.
*/
static final Entry<?,?>[] EMPTY_TABLE = {}; /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; ...
}

  1.8 的底层数据结构

/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; ...
} /**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; ...
}

  但基础结构还是: 数组 + 链表 ,称作 哈希表 或 散列表

  只是 1.8 做了优化,引进了 红黑树 ,来提升链表中元素获取的速度

JDK1.7 头插

  只有元素添加的时候,才会出现链表元素的插入,那么我们先来看看 put 方法

  put - 添加元素

    源码如下

    /**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
addEntry(hash, key, value, i);
return null;
}

    直接看代码可能不够直观,我们结合流程图来看

    什么? 还是不够直观? (楼主也这么觉得)

    那我们就结合具体案例来看下这个流程

    假设 HashMap 初始状态

    然后依次往里面添加元素:(2,b), (3,w), (5,e), (9,t), (16,p)

    再利用断点调试,我们来看看真实情况

    一切都对得上,进展的也挺顺利

  resize - 数组扩容

    上述提到了扩容,但是没细讲,我们来看看扩容的实现

    关键代码如下

/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
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];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
} /**
* Transfers all entries from current table to newTable.
*/
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;
}
}
}

    主要做了两件事:1、创建一个新的 Entry 空数组,长度是原数组的 2 倍,2、遍历原数组,对每个元素重新计算新数组的索引值,然后放入到新数组的对应位置

    有意思的是这个转移方法:transfer,我们结合案例来仔细看看

    假设扩容之前的状态如下图所示

    扩容过程如下

    利用断点调试,我们来看看真实情况

    链表元素的转移,还是采用的头插法

  链表成环

    不管是元素的添加,还是数组扩容,只要涉及到 hash 冲突,就会采用头插法将元素添加到链表中

    上面讲了那么多,看似风平浪静,实则暗流涌动;单线程下,确实不会有什么问题,那多线程下呢 ? 我们接着往下看

    将设扩容之前的的状态如下所示

    然后,线程 1 添加 (1,a) ,线程 2 添加 (19,n),线程 1 会进行扩容,线程 2 也进行扩容,那么 transfer 的时候就可能出现如下情况

    哦豁,链表成环了,这就会导致:Infinite Loop

JDK1.8 尾插

  1.8就不讲那么详细了,我们主要来看看 resize 中的元素转移部分

if (oldTab != null) {
// 从索引 0 开始逐个遍历旧 table
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 { // preserve order
// 拆链表,拆成两个子链表:索引不变的元素链表和有相同偏移量的元素链表
// 每个链表都保持原有顺序
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 // 通过尾部去关联 next,维持了元素原有顺序
loTail.next = e;
loTail = e;
}
else {
// 相同偏移量的元素链表
if (hiTail == null)
hiHead = e;
else // 通过尾部去关联 next,维持了元素原有顺序
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;
}
}
}
}
}

  通过尾插法,维护了链表元素的原有顺序

  在扩容时,头插法会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题,而尾插法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题

相关疑惑

  1、JDK 1.7及之前,为什么采用尾插法

    呃... 这个可能需要问头插法的实现者了;

    但有种说法,我觉得挺有道理:缓存的时间局部性原则,最近访问过的数据下次大概率会再次访问,把刚访问过的元素放在链表最前面可以直接被查询到,减少查找次数

  2、既然头插法有链表成环的问题,为什么直到 1.8 才采用尾插法来替代头插法

    只有在并发情况下,头插法才会出现链表成环的问题,多线程情况下,HashMap 本就非线程安全,这就相当于你在它的规则之外出了问题,那能怪谁?

    1.8 采用尾插,是对 1.7 的优化

  3、既然 1.8 没有链表成环的问题,那是不是说明可以把 1.8 中的 HashMap 用在多线程中

    链表成环只是并发问题中的一种,1.8 虽然解决了此问题,但是还是会有很多其他的并发问题,比如:上秒 put 的值,下秒 get 的时候却不是刚 put 的值;因为操作都没有加锁,不是线程安全的

总结

  1、JDK 1.7 采用头插法来添加链表元素,存在链表成环的问题,1.8 中做了优化,采用尾插法来添加链表元素

  2、HashMap 不管在哪个版本都不是线程安全的,出了并发问题不要怪 HashMap,从自己身上找原因

参考

  HashMap为何从头插入改为尾插入

  HashMap 中的容量与扩容实现,细致入微,值的一品!

HashMap 链表插入方式 → 头插为何改成尾插 ?的更多相关文章

  1. Java解释单链表中的头插法以及尾插法

    单链表属于数据结构中的一种基本结构,是一种线性结构,在此使用Java对其中的头插法以及尾插法进行解释. 首先定义好链表中的节点类: 其中,data代表节点所存放的数据,next代表指向下一节点 对于单 ...

  2. 将html页改成jsp的两种方式

    将html页改成jsp的两种方式 作者: 字体:[增加 减小] 类型:转载 时间:2013-08-13 将html页改成jsp有两种方法,第一种是直接修改html文件,另一种是新建jsp文件.下面为大 ...

  3. ASP.NET 使用mode=”InProc”方式保存Session老是丢失,无奈改成StateServer 模式。

    http://blog.csdn.net/fox123871/article/details/8165431 session是工作在你的应用程序进程中的.asp.net进程.iis往往会在20分钟之后 ...

  4. iOS9 http不能访问网络——在Xcode中将https改成http方式

    =====================2016-01-29更新=========================== 最近做demo时,发现将https改成http方式略有小变 1. 没有改成ht ...

  5. SQLyog软件里无法插入中文(即由默认的latin1改成UTF8编码格式)

    问题详情: 无法插入中文? 解决办法: 需要修改编码格式,由默认的latin1改为utf8. 改成, 成功!

  6. 关于富文本编辑器—UEditor(java版)的使用,以及如何将UEditor的文件/图片上传路径改成绝对路径

    突然发现好久没写博客了,感觉变懒了,是要让自己养成经常写文章的习惯才行.既可以分享自己的所学,和所想,和大家一起讨论,发现自己的不足的问题. 大家可能经常会用到富文本编辑器,今天我要说的是UEdito ...

  7. HashMap 链表和红黑树的转换

    HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树:若桶中元素小于等于6时,树结构还原成链表形式. 原因: 红黑树的平均查找长度是log(n),长度为8 ...

  8. direct-path插入方式提升性能的分析

    1.传统串行insert方式 常见的insert方式有两种: (1)      insert into table_name values(....) (2)      insert into tar ...

  9. Go 语言接口及使用接口实现链表插入

    @ 目录 1. 接口定义 1.1 空接口 1.2 实现单一接口 1.3 接口多方法实现 2. 多态 2.1 为不同数据类型的实体提供统一的接口 2.2 多接口的实现 3. 系统接口调用 4. 接口嵌套 ...

随机推荐

  1. day40 Pyhton 并发编程03

    一.内容回顾 进程是计算机中最小的资源分配单位 进程与进程之间数据隔离,执行过程异步 为什么会出现进程的概念? 为了合理利用cpu,提高用户体验 多个进程是可以同时利用多个cpu的,可以实现并行的效果 ...

  2. C++虚函数与多继承

    虚函数 C++用虚函数实现运行时多态,虚函数的实现是由两个部分组成的,虚函数指针与虚函数表. 虚函数指针(vptr)是指向虚函数表的指针,在一个被实例化的对象中,它总是被存放在该对象的地址首位.而虚函 ...

  3. 【C语言C++编程学习笔记】一种很酷的 C 语言技巧,灵活运用编程技巧让你写代码事半功倍!

    C语言常常让人觉得它所能表达的东西非常有限.它不具有类似第一级函数和模式匹配这样的高级功能.但是C非常简单,并且仍然有一些非常有用的语法技巧和功能,只是没有多少人知道罢了. ☆ 指定的初始化 很多人都 ...

  4. 适合刚刚学习编程的萌新:C语言编程学习制作超简单又好玩的报数游戏!

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  5. idea创建servlet工程初体验

    servlet工程创建 前提:创建项目之前需要配置java环境变量 和tomcat配置,配置完成后进入如下操作. tomcat 安装和配置参考 https://www.cnblogs.com/xush ...

  6. swoole热启动

    通过扫描指定的要扫描的目录,把所有文件找出来,分别md5 连接字符串,最后再md5返回 启动定时器,扫描,当前的加密值和以前一样不管,否则就重启服务,把当前赋值给旧值 . httpServer.php ...

  7. spring boot:spring security整合jwt实现登录和权限验证(spring boot 2.3.3)

    一,为什么使用jwt? 1,什么是jwt? Json Web Token, 它是JSON风格的轻量级的授权和身份认证规范, 可以实现无状态.分布式的Web应用授权 2,jwt的官网: https:// ...

  8. Java进阶专题(十五) 从电商系统角度研究多线程(下)

    前言 ​ 本章节继上章节继续梳理:线程相关的基础理论和工具.多线程程序下的性能调优和电商场景下多线程的使用. 多线程J·U·C ThreadLocal 概念 ​ ThreadLocal类并不是用来解决 ...

  9. Centos7 安装python环境

    保留python2 找到python所在位置,把python指向python2.7备份 [root@sun /usr/bin]# cd ~ [root@sun ~]# whereis python p ...

  10. mysql优化篇(基于索引)

    在上一篇文章:Mysql索引(一篇就够le) 中介绍了索引的基本使用,分类和原理,也强烈建议先读Mysql索引(一篇就够le),然后继续本文的阅读 我们也知道mysql的优化可以从很多的方面进行,比如 ...