HashMap源码解析(java1.8.0)
1.1 背景知识
1.1.1 红黑树
二叉查找树可能因为多次插入新节点导致失去平衡,使得查找效率低,查找的复杂度甚至可能会出现线性的,为了解决因为新节点的插入而导致查找树不平衡,此时就出现了红黑树。
红黑树它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。它具有以下特点:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(叶子节点,是指为空(NIL或NULL)的叶子节点)是黑色。
(4)如果一个节点是红色的,则它的子节点一定是黑色(即从根节点到叶子节点的路径上不能有两个重复的红色节点)。
(5)从一个节点到其上每一个叶节点的所有路径都具有相同的黑色节点个数。
红黑树的基本操作--添加
① 将红黑树当作一颗二叉查找树,将节点插入。
② 将插入的节点着色为"红色"。(因为条件5,从一个节点到其中每一个节点的的所有路径都具有相同的黑色节点)。
③通过一系列的旋转(左旋或右旋操作)或着色等操作,使之重新成为一颗红黑树。
1.2 源码
在java 1.7之前是用数组和链表一起组合构成HashMap,在java1.8之后就使用当链表长度超过8之后,就会将链表转化为红黑树,缩小查找的时间(红黑树维护也会花费大量时间,包含左旋、右旋和变色过程)。
1.2.1 HashMap的初始化
hashmap构造函数会初始化三个值:
- 初始容量initialCapacity:默认值是16,当储存的数据越来越多的时候,就必须进行扩容操作。
- 阈值threshold:hashmap的数组结构中所能存放的最大数量,超过该数量,则会对数组进行扩容。阈值的计算方式为:容量(initialCapacity)*负载因子(loadFactor)。
- 负载因子loadFactor:当负载因子很大时,阈值会很大,table数组扩容的可能性比较小,会使得一个数组中的链表(红黑树)存放过多的数据,虽然节省了一定的空间,但会导致查询时间很长。相反负载因子很小时,扩容的可能性会很高,使得数组中的数据变得相对少,查询时间会缩短,但会花费较长的时间。
在初始化一个hashmap对象的时候,指定键值对的同时,也可以指定初始map的容量大小,假设此处我们指定大小为11,则会在构造函数中调用tableSizeFor将容量改为2的n幂次,即比当前容量大,而且必须是2的指数次幂的最小数,就会变成16。这是因为2的指数次幂便于计算进行位运算操作,提升运行效率问题(位运算>加法>乘法>除法>取模)。
hashmap的的默认值是16,负载因子默认是0.75,代码如下:
//HashMap<String,String> hashMap = new HashMap<String, String>(11); /**
* Returns a power of two size for the given target capacity.
**/
static final int tableSizeFor(int cap) {
int n = cap - 1; //10 防止在cap已经是2的n次幂的情况下
// >>> 表示不关心符号位,对数据的二进制形式进行右移 |表示或运算
n |= n >>> 1; //15
n |= n >>> 2; //15
n |= n >>> 4; //15
n |= n >>> 8; //15
n |= n >>> 16; //15
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //16
}
1.2.2 HashMap的put操作
这里可以介绍一下&位运算,当我们将一对KV存储到hashmap当中时,会通过(n - 1) & hash运算来定位将要插入的键值对放入到哈希表的某个桶中。其中n表示哈希表的长度,通常n为2的倍数,通过n-1即可n所表示的二进制数,除最高位外,全部转化为1,借助与运算即可快速完成取模操作。
//hashMap.put("2020", "good luck"); /**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果hashtable没有初始化,则初始化该table数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过位运算找到数组中的下标位置,如果数组中对应下标为空,则可以直接存放下去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//数组元素对应的位置已经有元素,产生碰撞
Node<K,V> e; K k;
//如果插入的元素key是已经存在的,则将新的value替换掉原来的旧值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果此时table数组对应的位置是红黑树结构,则将该节点插入红黑树中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果此时table数组对应的位置是链表结构
for (int binCount = 0; ; ++binCount) {
//遍历到数组尾端,没有与插入键值对相同的key,则将新的键值对插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表过长,将链表转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//发现链表中的某个节点有与插入键值对相同的key,则跳出循环,在循环外部重新赋值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//该key在hashmap已存在,更新与在链表跳出循环节点对应的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//超过阈值则更新
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1.2.3 HashMap的get操作
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table数组不为空,且对应的下标位置也不为空。
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果第一个位置是对应的key,则返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//遍历其他元素
if ((e = first.next) != null) {
//红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
1.2.4 HashMap的扩容操作
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//table不为空,且容量大于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; // double threshold
}
//阈值大于0,直接使用旧的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果阈值为零,则使用默认的初始化值
else { // zero initial threshold signifies using defaults
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//将之前旧数组桶的数据重新移到新数组桶中
if (oldTab != null) {
//依次遍历旧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;
/*
* e.hash & oldCap 对hash取模运算,
* 虽然数组大小扩大了一倍,
* 但是同一个key在新旧table中对应的index却存在一定联系:
* 要么一致,要么相差一个 oldCap。
*/
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;
}
此处在处理链表的时候,如何将链表中的节点重新分配到新的哈希表需要做一些解释。在扩容的时候,将原来的哈希表扩大了一倍,原来属于同一个桶中的数据会被重新分配,此时取模运算时(a mod b),会注意到,b会扩大两倍(a mod 2b),此时如果该桶中的某一个数据的哈希值是c1(0<c<b),则它必定还是会落入原来的位置,而如果桶中的某一个数据的哈希值是c2(b<c2<2b),则它会被重新分配到一个新的位置(这个位置是原先的哈希桶位置+旧桶的大小)。
HashMap在多线程的情况下出现的死循环现象
在某些java版本中扩容机制如果使用链表,且再插入时使用尾插法会出现死循环,具体原因可以参考老生常谈,HashMap的死循环,在本文中所参考的java版本使用了头插法的方式将元素添加到链表当中,可以避免死循环的出现,但是会出现一部分节点丢失的问题。如图:
假设原始的哈希map的某个桶的数据如下,此时线程开始扩容,将桶中的数据分配到lo和hi桶的链表中。
初始时刻线程1和线程2开始运行,线程1在执行完以下代码后,线程1的时间片运行结束。线程1运行的结果如图所示
线程2与线程1同时运行,线程2的时间片未用完,还在继续执行,根据代码的分配策略,线程2直到时间片运行结束,出现如图所示的结果:
此时CPU的时间片又被分配到了线程1,线程1继续运行,因为此时A所在的链表结构已经发生了变化,只能处理A,B,D三个元素。此时线程1创建的hashmap如图:
参考资料
HashMap源码解析(java1.8.0)的更多相关文章
- 【转】Java HashMap 源码解析(好文章)
.fluid-width-video-wrapper { width: 100%; position: relative; padding: 0; } .fluid-width-video-wra ...
- HashMap源码解析 非原创
Stack过时的类,使用Deque重新实现. HashCode和equals的关系 HashCode为hash码,用于散列数组中的存储时HashMap进行散列映射. equals方法适用于比较两个对象 ...
- Java中的容器(集合)之HashMap源码解析
1.HashMap源码解析(JDK8) 基础原理: 对比上一篇<Java中的容器(集合)之ArrayList源码解析>而言,本篇只解析HashMap常用的核心方法的源码. HashMap是 ...
- 最全的HashMap源码解析!
HashMap源码解析 HashMap采用键值对形式的存储结构,每个key对应唯一的value,查询和修改的速度很快,能到到O(1)的平均复杂度.他是非线程安全的,且不能保证元素的存储顺序. 他的关系 ...
- HashMap源码解析和设计解读
HashMap源码解析 想要理解HashMap底层数据的存储形式,底层原理,最好的形式就是读它的源码,但是说实话,源码的注释说明全是英文,英文不是非常好的朋友读起来真的非常吃力,我基本上看了差不多 ...
- 详解HashMap源码解析(下)
上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法.本文重点讲解元素的添加.查找.扩容等主要方法. 添加元素 put(K ...
- HashMap 源码解析
HashMap简介: HashMap在日常的开发中应用的非常之广泛,它是基于Hash表,实现了Map接口,以键值对(key-value)形式进行数据存储,HashMap在数据结构上使用的是数组+链表. ...
- 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析
前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序. M ...
- 【Java深入研究】9、HashMap源码解析(jdk 1.8)
一.HashMap概述 HashMap是常用的Java集合之一,是基于哈希表的Map接口的实现.与HashTable主要区别为不支持同步和允许null作为key和value.由于HashMap不是线程 ...
随机推荐
- 用一个python文件去调用另一个python文件,关于相对路径的处理?
比如用a.py调用执行b.py,但是a.py和b.py路径环境不一样,而b.py中有使用相对路径读取文件,这时会报错,怎么在a.py中进行配置,使其调用b.py时路径和其相同,能否做到? 比如目录结构 ...
- windows假死原因调查
0. 现象 windows假死了,键盘功能正常,就是画面卡住不动了. 1. 看log linux下面很容易通过命令dmesg和/var/log/message来看日志. 但是windows就懵逼了,不 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 自定义仓储之增删改查
上一篇文章(https://www.cnblogs.com/meowv/p/12913676.html)我们用Code-First的方式创建了博客所需的实体类,生成了数据库表,完成了对EF Core的 ...
- 「雕爷学编程」Arduino动手做(25)——MQ2气敏检测模块
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...
- 使用urllib
urlopen的基本用法: 工具为:python3(windows) 其完整表达式为: urllib.request.urlopen(url, data=None, [timeout, ]*, caf ...
- 如何在本地调试你的 Spark Job
生产环境的 Spark Job 都是跑在集群上的,毕竟 Spark 为大数据而生,海量的数据处理必须依靠集群.但是在开发Spark的的时候,不可避免我们要在本地进行一些开发和测试工作,所以如何在本地用 ...
- Centos7中磁盘管理及扩展
前提要求: 虚拟机:centos7 虚拟机软件:VMware Workstation 12 在安装Centos系统时,磁盘选择为LVM逻辑卷.当选择为LVM后才能创建逻辑卷等(必须) 数据格式选择的是 ...
- SPL常用函数
使用SPL_AUTOLOAD_REGISTER装载类 <?php /** * libs/Test.class.php */ class Test { function __construct() ...
- 微软Azure IoT驱动数字化变革线上分享会(6月4号)
微软Azure IoT驱动数字化变革线上分享会(6月4号) 微软作为全球范围内IoT领域的领军者,以微软智能云Azure为基础和核心,推动包括物联网.机器学习.微服务.人工智能等在内的新技术的发展 ...
- 机器人操作系统——ROS,Robot Operating System
Note:偶然看到的滴滴研究院的无人驾驶竞赛,了解一下. ROS:专为机器人软件开发所设计出来的一套电脑操作系统架构.是一个开源的元级操作系统(后操作系统),提供类似操作系统的服务,包括硬件抽象描述. ...