目录
HashMap
参考文章:美团
https://zhuanlan.zhihu.com/p/21673805
put方法
源码解析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法
// 如果数组为空,即不存在Hash冲突,则直接插入数组
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 3.插入时,如果发生Hash冲突,则依次往下判断
else {
HashMap.Node<K,V> e; K k;
// a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value
// 判断原则equals() - 所以需要当key的对象重写该方法
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// b.继续判断:需要插入的数据结构是红黑树还是链表
// 如果是红黑树,则直接在树中插入 or 更新键值对
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果是链表,则在链表中插入 or 更新键值对
else {
// i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key
// 如果存在相同的,则直接覆盖
// ii.遍历完毕后任务发现上述情况,则直接在链表尾部插入数据
// 插入完成后判断链表长度是否 > 8:若是,则把链表转换成红黑树
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);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 对于i 情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 插入成功后,判断实际存在的键值对数量size > 最大容量
// 如果大于则进行扩容
if (++size > threshold)
resize();
// 插入成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
return null;
}
流程
- 调用 hash() 方法计算哈希值
- 如果当前哈希表内容为空,新建一个哈希表
- 如果要插入的桶中没有元素,新建个节点并放进去
- 否则从桶中第一个元素开始查找哈希值对应位置
- 如果桶中第一个元素的哈希值和要添加的一样,替换,结束查找,返回旧值
- 如果第一个元素不一样,而且当前采用的还是 JDK 8 以后的树形节点,调用 putTreeVal() 进行插入
- 否则还是从传统的链表数组中查找、替换,结束查找,返回旧值
- 当这个桶内链表个数大于等于8,就要调用 treeifyBin() 方法进行树形化
- 当桶内元素个数小于等于6时,红黑树退化为链表
- 键值对数量+1,最后检查是否需要扩容,键值对数量>某一阈值则扩容
hash计算方法,扰动函数
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- h >>> 16:无符号右移动,使高16位参与运算,避免数值较小时,只靠低位数据来计算哈希时导致的冲突
resize()方法
- 触发时机
- map初始化,新初始化哈希表时,容量为默认容量,阈值为 容量*加载因子
- map中键值对数量大于某阈值,扩容时容量和阈值均翻倍
- 当前元素hash值与原容量与运算结果为0,则扩容后位置不变,不为零则扩容后位置为原位置的基础上偏移原容量。
- 源码解析
/**
* 该函数有2中使用情况:1.初始化哈希表;2.当前数组容量过小,需要扩容
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;// 扩容前的数组(当前数组)
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 扩容前的数组容量(数组长度)
int oldThr = threshold;// 扩容前数组的阈值
int newCap, newThr = 0;
if (oldCap > 0) {
// 针对情况2:若扩容前的数组容量超过最大值,则不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 针对情况2:若没有超过最大值,就扩容为原来的2倍(左移1位)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 针对情况1:初始化哈希表(采用指定或者使用默认值的方式)
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);
}
// 计算新的resize上限
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) {
// 把每一个bucket都移动到新的bucket中去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果当前数组索引下存储有链表
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 当前链表只有一个节点
if (e.next == null)
// 直接重新hash
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;
// 计算hash值的最高位是0还是1,ondCap是2的整数次幂
if ((e.hash & oldCap) == 0) {// 如果最高位是0则说明扩容后链表位置不变
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {// 高位是1,链表位置移动一个oldCap的距离
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;
}
先对比hashcode,再用equals()方法
p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))
与1.7相比
不同 | JDK 1.7 | JDK 1.8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化方式 | 单独函数:inflateTable() | 直接集成到了扩容函数resize()中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法(先将原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
线程安全相关
- 数据覆盖,两个线程同时取得了e,则会丢失数据
- 数据丢失,如果两个线程有同样的hash值,且同时进入以下代码
if ((p = tab[i = (n - 1) & hash]) == null)// 如果没有hash碰撞则直接插入元素 tab[i] = newNode(hash, key, value, null);
- 循环链表,头插法导致的,jdk8采用尾插法解决了hashmap的线程不安全体现在哪里? - 多媒体光盘的回答 - 知乎
https://www.zhihu.com/question/28516433/answer/281307231
LinkedHashMap
- 可以按插入顺序或访问顺序获取元素,有accesOrder变量控制
- 重写了newNode方法,每次插入新节点的时候在双向链表尾部插入节点
hashtable
所有方法使用synchronized修饰
ConcurrentHashMap
jdk1.7
参考文章
结构
实现
- 通过分段锁技术保证并发环境下的写操作;
- Segment 继承自ReentrantLock,内存有HashEntry数组,可以充当锁
- 并发度为Segment的数量
jdk1.8
- ConcurrentHashMap是直接采用数组+链表+红黑树来实现,时间复杂度在O(1)和O(n)之间,如果链表转化为红黑树了,那么就是O(1)到O(nlogn)。
- 在put的时候ConcurrentHashMap会判断tabAt(tab, i = (n - 1) & hash)是不是 null,是的话就直接采用CAS进行插入,而如果不为空的话,则是synchronized锁住当前Node的首节点,这是因为当该Node不为空的时候,证明了此时出现了Hash碰撞,就会涉及到链表的尾节点新增或者红黑树的节点新增以及红黑树的平衡,这些操作自然都是非原子性的。从而导致无法使用CAS,当Node的当前下标为null的时候,由于只是涉及数组的新增,所以用CAS即可。