HashMap核心原理及源码分析



本文主要解读 ​​JDK1.8​​ 的实现,部分内容会与 ​​JDK1.7​​ 的实现进行对比。另外​​ConcurrentHashMap​​是​​HashMap​​的线程安全版本,其原理类似,故不会完全解读源码。

1.HashMap

1.1 JDK1.8中HashMap的实现

在JDK1.8中,​​HashMap​​是基于 数组+链表/红黑树 。

重要属性

硬背没意义,理解 自动扩容put 才是关键

// 默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 扩容因子
static final int DEFAULT_LOAD_FACTOR = 0.75f;

// 树化的阈值
static final int TREEIFY_THRESHOLD = 8;

// 树退化的阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 树化数组最小值
static final int MIN_TREEIFY_CAPACITY = 64;


1.1.1 扩容原码

虽然有点长,但一定坚持要看完。

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) {
// 最大长度
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新数组容量 < 最大数组容量 并且 原数组容量 >= 16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 扩容为 2 倍
newThr = oldThr << 1; // double threshold
}
// 原数组容量 = 0
// 原数组扩容阈值 > 0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 数组容量和数组扩容阈值都为0 说明没有初始化 没有传参 使用默认值
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) {
// 遍历原数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 存在元素 且用e保存起来
if ((e = oldTab[j]) != null) {
// 置空数组中当前下标位置元素 便于垃圾回收
oldTab[j] = null;
// 单个元素直接移动
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 树化的结构走红黑树移动逻辑
else if (e instanceof TreeNode)
// split方法中包含树退化的逻辑
((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;
// 低位
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;
}


自动扩容中有几个显而易见的设计是非常巧妙的。

容量始终是2的指数次幂,这样设计在​​resize()​​中看来有两个好处:

  1. 扩容直接使用位运算​​newThr = oldThr << 1;​​,效率更高(忽略),代码优雅。
  2. 在移动元素的时候,和高低位指针的设计完美结合,不需要二次hash。

高低位指针本身就很巧妙,即不二次hash,又完成了散列。

1.1.2 添加元素源码

// 虽然说put本身只调用putVal 但是它有2个写死的参数需要了解
public V put(K key, V value) {
// 第三个参数:onlyIfAbsent – 如果为真,则不更改现有值。很明显,我们使用HashMap时put相同的key会覆盖,这里false也印证了
// 第四个参数:evict – 如果为 false,则表处于创建模式。 用到这个参数的方法是一个空方法,所以不用管
return putVal(hash(key), key, value, false, true);
}
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)
// 调用resize方法初始化数组
n = (tab = resize()).length;
// 通过将要put的元素的hash确定元素下标位置 并判断该位置是否有元素
if ((p = tab[i = (n - 1) & hash]) == null)
// 为空,则说明没有元素,直接加入新节点即可
tab[i] = newNode(hash, key, value, null);
// 不为空 说明出现了hash冲突
else {
Node<K,V> e; K k;
// 判断要put元素的hashcode 及 key 是否与该位置已有元素相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 相同则赋值给e
e = p;
// 看该位置元素是否为红黑树
else if (p instanceof TreeNode)
// 走红黑树的put逻辑
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);
// binCount >= 7,第8次循环,但上一行已经添加了一个元素,所以是数组元素超过8才树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化
treeifyBin(tab, hash);
// next为空说明遍历结束
break;
}
// 走过上面的代码说明 e 不是末尾节点
// put的元素和对应位置的元素的key,key的hashcode是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p 指向下一个节点
p = e;
}
}
// 这里 e != null,走了上面 else逻辑
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 这个条件的前半段一定为 true
if (!onlyIfAbsent || oldValue == null)
// 替换值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 数组占用空间超过阈值
if (++size > threshold)
// 扩容
resize();
afterNodeInsertion(evict);
return null;
}


1.1.3 获取值

// 同put方法,门面
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 数组不为空 && 数组长度大于0 && 传入的key的hash值与上数组长度-1不为空(找到下标且元素不为空)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查传入key的hash是否与该下标元素hash码相同
if (first.hash == hash && // always check first node
// key内容是否相同
((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.1.4 链表树化

final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组长度<64 则会对数组进行扩容 而不是树化
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);
// 遍历第一次记录红黑树根节点
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);
}
}
// 构建红黑树
// 需要注意的是treeify是HashMap类的静态内部类的方法,不然看不懂下面的this
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 遍历逻辑 这里的this实际上是调用该方法的节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// 下一个节点
next = (TreeNode<K,V>)x.next;
// x的左右置空
x.left = x.right = null;
// 如果还没有根节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
// 根节点已存在的逻辑
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点开始查找
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 这里要注意之前逻辑的赋值操作
if ((ph = p.hash) > h)
dir = -1; // 向左查找
else if (ph < h)
dir = 1; // 向右查找
// 相等则判断key
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);

TreeNode<K,V> xp = p;
// 这里dir就是选择左还是右的依据
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 进行红黑树的插入平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}


1.1.5 红黑树退化

final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
// 生成链表节点
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}


1.1.6 删除元素

public V remove(Object key) {
Node<K,V> e;
// 参数说明参考下面的原函数
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// value: 调用方默认为空
// matchValue:调用方默认为false 配合value使用
// movable:调用方默认为true 表示删除元素时会移动其它元素
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 数组不为空 && 数组长度不为0 && 传入key定位的下标位置存在元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// 声明一堆局部变量
Node<K,V> node = null, e; K k; V v;
// 判断要删除的key的hash 和 找到数组对应下标元素(头)的hash 是否相同 && key内容是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p; // 记录
else if ((e = p.next) != null) {
// 红黑树删除
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 链表删除节点
else {
do {
// 同头节点判断
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// node != null 说明要删除的key是存在的
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// mode == p 只会是头节点 对应上面的if逻辑
else if (node == p)
tab[index] = node.next;
// 移除链表节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}


1.2 JDK1.8 HashMap核心原理小结

1.2.1 底层数据结构

数组 + 链表 + 红黑树

1.2.2 自动扩容原理

  • 创建一个2倍容量的新数组
  • 如果是红黑树则调用红黑树的逻辑;如果是链表则引入高低位指针进行移动和散列

1.2.3 put流程

  • 先检查数组是否初始化,未初始化则调用​​resize()​​方法初始化
  • 如果对应下表为空则直接创建节点加入
  • 如果对应下表是红黑树则走红黑树的插入逻辑
  • 如果是链表,则遍历,如果有相同的key则替换,没有则在末尾追加
  • 在遍历链表的时候会检查链表长度(实质是长度=9会树化),如果长度超过8则会进行树化判断​​treeifyBin()​​,如果数组容量达到64则树化,反之优先扩容数组​​resize()​

1.2.4 get流程

get比较简单,根据key去定位数组下标,再遍历即可。

1.2.5 remove流程

  • 首先要检查给定key是否存在,不存在直接结束方法
  • 如果要删除的元素在红黑树结构中,会先获取树节点,再​​removeTreeNode​​,这里也会检查树是否退化
  • 如果要删除的元素再链表结构中,先获取链表节点,再删除

1.2.6 关于HashMap自带属性的默认值

  • 初始容量16:没有为啥,设计者就这么写的
  • 最大容量 1 << 30:计算机限制 Integer.Max_Value
  • 扩容因子 0.75f:牛顿二项式推到出来的最优值越0.69,Java取大了一点,应该是出于时间和空间平衡的考虑
  • 树化阈值8 & 退化阈值6:时间复杂度
  • 链表树化数组最小容量64:泊松分布算的,概率问题

1.3 JDK1.8 和 JDK1.7中HashMap的一点区别

JDK1.7中HashMap采用的是 数组 + 链表 的结构实现的。另外,再扩容方面,JDK1.7是对链表进行遍历,然后计算hashcode重新散列到新数组,并且在移动的过程中采用的是头插法,即新老数组中链表结构会反转。

JDK1.7中扩容采用的头插法,在并发场景下会出现问题,就是链表成环,导致后续无法put元素,或者get,因为会进入死循环。

2.ConcurrentHashMap

ConcurrentHashMap​HashMap​原理类似,只是它考虑了线程安全问题,使用了​synchronized​和一些​CAS​的方法

2.1 JDK1.8 ConcurrentHashMap

其内部维护的很多变量和HashMap一样。

数据结构:

synchronized + CAS + Node + 红黑树

锁:

  • 读操作无锁,volatile保证,修饰Node的val和next。
  • 写操作加锁,锁链表的头节点,不影响其它元素的读写,锁的粒度更细,效率更高。
  • 扩容时阻塞所有读写操作,并发扩容

2.2 JDK1.7中ConcurrentHashMap

采用分段锁(Segment)的方式来实现加锁。一个Hash表中套一个Hash表。锁的粒度大。

数据结构:

ReentrantLock + Segment + HashEntry

Segment是一个HashEntry的数组,每个元素都是链表结构(或空)。

元素查询:

需要两次hash,第一次查询Segment,第二次定位到元素链表的头部

get方法无需加锁,volatile保证

锁:

Segment继承了ReentrantLock。加锁会锁定整个Segment,锁粒度大,但是不会影响其它Segment。并发度等于Segment数,扩容不会影响其它Segment。每个Segment内部会进行扩容。