文章目录
JDK1.7中HashMap的put()方法全过程。
JDK1.8中HashMap的put()方法全过程。
源码及注释
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 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;
//步骤①:如果Table为空,初始化一个Table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//步骤②:如果该bucket位置没值,则直接存储到该bucket位置
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;
//步骤④:如果该bucket位置数据是TreeNode类型,则将新数据添加到红黑树中。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //步骤⑤:如果该链为链表
for (int binCount = 0; ; ++binCount) {
//添加到链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //如果链表个数达到8个时,将链表修改为红黑树结构
break;
}
// key已经存在直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//更新键值,并返回旧值
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;
}
步骤总结
①. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
JDK1.8有那些变化。
JDK1.7中的HashMap
基于链表+数组实现,底层维护一个Entry数组
Entry<K,V>[] table;
根据计算的hashCode将对应的KV键值对存储到该table中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时,形成了一个链表式的存储结构,如下图
JDK1.8中的HashMap
基于位桶+链表/红黑树的方式实现,底层维护一个Node数组
Node<K,V>[] table;
在JDK7中HashMap,当成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不可避免的花费O(N)的查找时间,这将是多么大的性能损失,这个问题终于在JDK8中得到了解决。
JDK8中,HashMap采用的是位桶+链表/红黑树的方式,当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这是JDK7与JDK8中HashMap实现的最大区别。
如下图所示:
异同
共同点
- 容量(capacity):容量为底层数组的长度 容量一定为2的次幂
/**
这段代码是用来计算出键值对存放在一个数组的索引,h是int hash = hash(key.hashCode())计算出来的,SUN大师们发现, “当容量一定是2^n时,h & (length - 1) == h % length” ,按位运算特别快 。
源码中大量使用运算,对于计算机,位运算计算效率特别快,毕竟二进制才是亲儿子呀。
*/
static int indexFor(int h, int length) { return h & (length-1); }
- 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
a. 默认加载因子 = 0.75
/**
加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
0.75是一个"冲突的机会"与"空间利用率"之间寻找一种平衡与折衷的选择
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f
- 扩容机制:扩容时resize(2 * table.length),扩容到原数组长度的2倍
- key为null::若key == null,则hash(key) = 0,则将该键-值 存放到数组table 中的第1个位置,即table [0]
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
不同点
- 发生hash冲突时
JDK7:发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,就元素移动到链表中。
JDK8:发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,如果大于8要转成红黑树。 - 扩容时
JDK7:在扩容resize()过程中,采用单链表的头插入方式,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。
多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。
JDK8:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。
建议
- 使用时设置初始值,避免多次扩容的性能消耗
- 使用自定义对象作为key时,需要重写hashCode和equals方法
- 多线程下,使用CurrentHashMap代替HashMap
JDK1.7当中HashMap中线程不安全问题有那些?原因分别是什么?
扩容时不安全
线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。
JDK1.8之后如何链地址法,链表长度是多少的时候会转换成红黑树。
长度为8的时候会转换成红黑树
JDK1.8节点个数是多少的时候,红黑树会退回链表。
小于6的时候。
为什么会选择8作为链表转红黑树的阈值。
在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
HashMap与HashTable有什么区别?
HashTable
- 底层数组+链表实现,无论key还是value都不能为null,线程安全, 实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- 初始size为11,扩容:newsize = olesize*2+1
- 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)