在Java 7及之前的版本中,HashMap的底层数据结构主要是数组加链表。具体实现如下:
Entry<K,V>[] table
),这个数组的大小总是2的幂。每个数组元素是一个单一的Entry节点,或者是一个链表的头节点。从Java 8开始,HashMap的底层结构除了数组加链表之外,还引入了红黑树,以优化在链表过长时的查找性能。结构变为数组加链表加红黑树:
resize()
),链表会被转换成红黑树。这种转换提高了在大量哈希冲突情况下的查找效率,因为红黑树的查找时间复杂度为O(log n),相较于链表的O(n)有显著提升。在JDK1.7的时候,采用的是头插法
在JDK1.8改成了尾插法,这是因为头插法在多线程环境下扩容时可能会产生循环链表问题
无论是JDK1.7还是1.8都是线程不安全的,下图是1.8中的put方法
tab是就是HashMap的数组table,第一个if判断时做了赋值。框起来的部分表示如果没有hash冲突就直接在数组上插入元素,但是如果两个线程hash一样且都进入到了这个if下,线程1先执行的插入数据,线程2会覆盖1插入的数据。
那么什么是循环链表问题呢?这就不得不介绍一下HashMap的扩容机制了。
首先讲几个HashMap的属性
当size大于threshold时,就会进行扩容resize()
。
扩容分为两步
为什么要重新计算index值,直接把张三这条链表复制到新的数组中不行吗?
答案是不行的,因为index规则是根据数组长度来的,如图
所以index 的计算公式是这样的:
循环链表问题理解起来比较麻烦,如果理解不了就知道JDK1.7HashMap扩容的时候有这么回事就行。但是可能是我自己太笨了万一大家一看就懂了呢
我们来看一下Java1.7扩容的源码
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
//重新计算元素在数组中的索引
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
重点在于transfer方法,作用是重新计算index值然后将旧数组中的数据迁移到新数组
循环链表的产生:
原因:
假设我们有两个Thread都在执行resize方法,Thread1第一步刚执行完第23行Entry<K,V> next = e.next;
就卡住了,这时Thread2执行完了resize方法。
过程:
由此我们知道了循环链表产生的关键在于头部插入元素A时,元素A的next元素B的next是元素A本身,所以Java8采用了尾插法,避免了循环链表。
求赞!求关注!!以后会更新更多有用的内容!!!꒰⑅•ᴗ•⑅꒱
保佑大家代码永无bug ╰(´︶`)╯