HashMap和HashMap都是基于哈希表实现的,其内部的每个元素都是key-value键值对,HashMap和HashTable都实现了Map、Cloneable、Serializable接口
线程安全性:HashMap线程不安全,HashTable安全
性能:虽然都是基于单链表,但是由于HashTable的put、get、remove操作加了synchronized锁,所以效率低
初始容量:
HashTable初始长度11,每次扩容变为2n+1
HashMap初始容量16,每次扩充变为原来的两倍
创建给定初始容量时,HahsTable会直接使用给定大小,HashMap扩充到2的幂次方
Node节点是用来存储HashMap的一个个实例,它实现了Map.Entry接口,我们先来看一下Map中的内部接口Entry接口的定义
一个map的entry链,是这个Map.entrySet()返回的一个集合视图,包含了各种的元素
这个唯一的方式是从集合的视图进行迭代,获取一个map的entry链,这个Map.Entry链只在迭代期间有效
Node节点会存储四个属性,hash值,key,value,指向下一个Node节点的引用
因为Map.Entry是一条条entry链连接在一起的,所以Node节点也是一条条entry链
继承于AbstractSet抽象类,它是由HashMap中的keyset()方法来创建KeySet实例的,旨在对HashMap中的key键进行操作
/**
* 返回一个set视图,这个视图中包含了map中的key
**/
public Set<K> keySet() {
// keySet 指向的是 AbstractMap 中的 keyset
Set<K> ks = keySet;
// 如果 ks 为空,就创建一个 KeySet 对象
if (ks == null) {
// 并对 ks 赋值
ks = new KeySet();
keySet = ks;
}
return ks;
}
和keyset类似,只是针对key-value键值对中的value值进行使用
对key-value键值对进行操作的内部类
JDK1.7中,HashMap采用位桶+链表的实现,即使用链表来处理冲突,同一hash值的链表都存储在一个数组中。但是当未育一个桶中的元素较多,即hash值相等的元素较多时,通过key值以此查找的效率较低
HashMap大致结构
HashMap底层数据就是一个Entry数组,Entry是HashMap的基本组成单元,每个Entry中包含一个key-value键值对,每个Entry包含 【hash,key,value】 属性,如图:
与JDK1.7相比,1.8在底层结构方便做了一些改变,当每个桶中元素大于8的时候,会转变为红黑树,目的就是优化查询效率,JDK1.8重写了resize()方法
HashMap的默认容量为: 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap最大容量为: 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
Q?int占用4个字节,按说最大容量应该是左移31位
A:数值计算中,最高位也就是最左边的位数为符号位,代表正负,0->正数,1->负数
HashMap默认负载因子为:0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
扩容机制的原则是当HashMap中存储的数量>HashMap容量*负载因子时,就会把HashMap的容量扩大为原来的两倍。
HashMap的第一次扩容就是在 16*0.75=12 时进行
HashMap的树化阈值为:8
static final float DEFAULT_LOAD_FACTOR = 0.75f;
在添加元素时,当一个桶中存储元素的数量>8时,会自动转化成红黑树
HashMap的链表阈值为:6
static final int UNTREEIFY_THRESHOLD = 6;
在进行删除元素时,如果一个桶中存储元素数量<6之后,会自动跳转为链表
64
static final int MIN_TREEIFY_CAPACITY = 64;
这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化
HashMap中的节点数组就是Entry数组,它代表的就是HashMap中 【数组 + 链表】 数据结构中的 数组
Node数组在第一次使用的时候进行初始化操作,在必要的时候进行resize,resize后的数组的长度扩容为原来的二倍
在HashMap中,使用 size 来表示HashMap中键值对的数量
在HashMap中,使用modCount来表示修改次数,主要用于做并发修改HashMap时的快速失败,即:
-fail-fast机制
在HashMap中,使用 threshold 表示扩容的阈值,也就是 初始容量*负载因子的值
这个问题由 tableSizeFor() 源码解决的
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
loadFactory表示负载因子,它表示的是HashMap的密集程度
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//首次初始化的时候table为null
n = (tab = resize()).length;//对HashMap进行扩容
if ((p = tab[i = (n - 1) & hash]) == null)//根据hash值来确认存放的位置。如果当前位置是空直接添加到table中 PS: 多线程情况下可能出现值覆盖
tab[i] = newNode(hash, key, value, null);
else { //如果存放的位置已经有值
Node<K, V> e;
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//如果插入位置key重复
e = p;
else if (p instanceof TreeNode)//如果插入的位置正好是红黑树
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);//将数据put到红黑树
else {//如果以上条件均不满足 就说明还是链表
for (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) { //遍历到链表的尾节点
p.next = newNode(hash, key, value, null); //新建一个链表节点(可以看出HashMap链表是尾插的)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 判断链表长度是否大于8(binCount是从0开始)
treeifyBin(tab, hash);//如果循环了达到了红黑树的阈值,也就是说这里的链表长度大于8,然后就把这个结构树形化。
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//在循环的过程中遇到了key值相同的节点,跳出循环
p = e;//和前面for中的p.next对应,进行循环遍历链表。
}
}
if (e != null) { // existing mapping for key 如果e!=null 说明存在相同的key
V oldValue = e.value;//记录下原来的key对应的value
if (!onlyIfAbsent || oldValue == null)//当onlyIfAbsent为false或者旧值为空
e.value = value;//替换新的value并返回旧的value
afterNodeAccess(e);//HashMap中暂无实现
return oldValue;//返回旧值,这样就处理掉了前面的当key值相同时的情况了。
}
}
++modCount;// 每次容器发生变化记录
if (++size > threshold)// 实际大小大于阈值则扩容
resize();//如果当前HashMap的容量超过threshold则进行扩容
afterNodeInsertion(evict);//HashMap中暂无实现
return null;
}
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K, V> hd = null, tl = null;
do {
TreeNode<K, V> p = replacementTreeNode(e, null);//将Node节点变换为TreeNode节点
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);//转换成双向链表
if ((tab[index] = hd) != null)
hd.treeify(tab);//将链表树化
}
}
当桶中无元素 ->O(1)
当桶中元素为链表时 -> O(1)+O(n)
当桶中元素为红黑树时 -> O(1)+O(logn)
HashMap底层是使用桶+链表实现的,位桶决定元素的插入位置,位桶是有hash方法决定的,当多个元素的hash计算得到相同的哈希值后,HashMa会把多个Node元素都放在对应的位桶中个,形成链表,这种处理哈希碰撞的方式被称为链地址法
首先会检查 table 中的元素是否为空,然后根据 hash 算出指定 key 的位置。然后检查链表的第一个元素是否为空,如果不为空,是否匹配,如果匹配,直接返回这条记录;如果匹配,再判断下一个元素的值是否为 null,为空直接返回,如果不为空,再判断是否是 TreeNode 实例,如果是 TreeNode 实例,则直接使用 TreeNode.getTreeNode 取出元素,否则执行循环,直到下一个元素为 null 位置。
HashMap 中有两个非常重要的变量,一个是 loadFactor ,一个是 threshold ,loadFactor 表示的就是负载因子,threshold 表示的是下一次要扩容的阈值,当 threshold = loadFactor * 数组长度时,数组长度扩大位原来的两倍,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。
查找数组对应的位置需要通过取余计算,HashMap源码中是通过位运算计算数据下标:
int index = hash & (length - 1)
主要是通过key的hash值与数组长度减一做与运算,而要满足于取余预算结果相等的条件,数组长度只能为2的幂次方。
由于length-1一定是基数,因此二进制最后一位永远为1,又是因为与运算,因此最终结果可能为0也可能为1,尽可能的保证分布的松散型,减少hash碰撞
扰动函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的地位掺杂了高位的部分特征,这样高位的信息也被变相的保留下来
new HashMap()的时候只是进行参数传递,真正初始化是在put元素的时候执行resize()
目的:性能优化,节约堆内存
关键词:泊松分布
LinkedList转化成红黑树非常消耗性能,要尽可能避免
同样我们也希望数据直接落在table中,尽可能少形成链表或者长链表,因此根据泊松分布计算得出(概率学),链表长度达到8的概率已经是极低了,根据源码注释可知,HashMap桶中90%都是0-1长度的链表,性能很高
/** threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
**/
历史:JDK7时存在一个bug,Apache Tomcat使用哈希表存储HTTP请求参数时,由于哈希表冲突引发Dos漏洞(服务器仍在运转却拒绝用户访问)
解决方法:主要是因为hash冲突导致链表过长,因此在JDK1.8中引入了红黑树,减少因为链表过长导致的查询速度过慢问题
补充:Tomcat在JDK1.8发布之前采用了一个伤害不大,侮辱性极强的解决方案,引入参数限制处理的参数数量,默认10000
简要描述:扩容会执行链表循环,当Node.next() == null时,跳出循环,多线程会打乱这段逻辑
符合优先使用最近的数据原则,新插入的更有可能被用到
HashMap是综合效率最高的数据结构,因此也需要选择综合效率最高的红黑树
平衡二叉树付出很大的代价形成结构,因此采用红黑
数据结构基本与HashMap相似,只是对map操作的方法都加上了synchronized关键字,全表锁,性能很低
分段锁,默认16个分段锁
引入cas操作:put元素至数组时,采用cas无锁操作,判断链表是否为空
引入synchroized关键字锁住node节点本身
有可能脏读,读出数据并非实时