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不是线程 ...
随机推荐
- Dockerfile-Namespace
Docker核心-Namespaces(命名空间) 1)概念: 命令空间是Linux内核的一个强大的特性.每个容器都有自己单独的命令空间,运行在其中的应用都是独立在操作系用中运行一样.命名空间保证了容 ...
- lb的keepalive问题
lb的keepalive问题 0. keepalive 大家都很清楚他的用意了,就是为了减少3次握手,设置一个timeout,比如说20s ,在20s内不请求,连接还是保持着,这时候请求过来,不需要重 ...
- 转帖 支撑4.5亿活跃用户的WhatsApp架构概览
http://www.csdn.net/article/2014-02-27/2818559-an-overview-at-whatsapp's-19b-architecture/2 写的很好,确实牛 ...
- MySQL数据库基础操作语句
SQL语言主要用于存取数据.查询数据.更新数据和管理关系数据库系统,分为3种类型: 1.DDL语句 数据库定义语言: 数据库.表.视图.索引.存储过程,例如CREATE DROP ALTER 2.DM ...
- 使用urllib
urlopen的基本用法: 工具为:python3(windows) 其完整表达式为: urllib.request.urlopen(url, data=None, [timeout, ]*, caf ...
- 文本分类—day00_导读
新公司有文本分类的服务,看上去很高级,想探究一下里面的东东.并且最近人工智能,深度学习实在是太火了,出去聊天,不会点cnn算法,都不好意思搭话.后面会出文本分类相关的内容,希望能做到类似实验楼一样的实 ...
- 基于postman测试接口(整套接口测试)
基于postman测试接口(整套接口测试) 可以解决的问题 几百个接口人工测试接口过于繁杂 大多测试无法使用请求结果当参数 可以使用随机参数 支持swagger信息导入 随账号持久化保存数据 对集合一 ...
- eclipse——Error exists in required project Proceed with launch?
运行java文件时报错: Error exists in required project Proceed with launch? 报错截图: 问题参生原因:开始Buildpath了一个jar ...
- JS获取两个日期间的所有日期
var stime = '2018-07-25'; //开始日期 var etime = '2018-08-02'; //结束日期 getdiffdate(stime,etime); //获取两日期之 ...
- Javascript输入输出语句
方法 说明 归属 alert(msg) 浏览器弹出警示框 浏览器 console.log(msg) 浏览器控制台打印输出信息 浏览器 prompt(info) 浏览器弹出输入框,用户可以输入 浏览器 ...