1、JDK1.7
- HashMap的结构为:数组+链表
- 采用头插法,发生碰撞的时候,
新元素指向旧元素 。 - 存在的问题:链表循环。
1.1、HashMap源码
-
put()方法: public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
-
addEntry(): void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
-
resize(): 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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
-
transfer(): 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);
}
}
}
- 负载因子是0.75,初始容量是12,扩容的阈值就是12。
1.2、链表循环的原因
- 在上面已经知道了HashMap在放入元素会经过put()、addEntry()、以及扩容的时候resize()、transfer()方法。问题其实就是处在transfer()方法上。
- 假设现在HashMap的大小为2,放入的元素为3,5,7,都冲突在了数组下标1的位置,然后Hash表扩容。这是单线程的情况。
- 假设有两个线程呢?
当线程1执行到do-while语句 第一句的时候就被调度挂起,那么情况如图: 那么这里意思就是:线程1还没有完全扩容,但是e和next都已经指向了线程2正常扩容的了。 当线程1重新被调度回来执行的时候:
newTalbe[i] = e; ,此时线程1的HashMap数组下标3的位置指向了Key(3)。e=next; ,此时e指向了Key(7)- 而下次循环的
next=e.next; ,就导致了next指向了Key(3) - 继续执行
newTable[i] = e; ,那么情况如下图,接着e和next接着下移 - 此时Key(7).next=Key(3),而此时e指向了Key(3),继续执行
newTable[i] = e; ,那么Key(3).next=Key(7),循环出现了。 - 线程2生成的e和next的关系影响到了线程1的情况。从而打乱了正常的e和next的链。于是,当我们的线程一调用到,HashTable.get(11)时,即又到了3这个位置,需要插入新的,那这会就e 和next就乱了。
2、JDK1.8
2.1、结构变化
- 结构变为了数组+链表+
红黑树 。 - 具体触发条件为:某个
链表 的个数大于8 ,并且数组的大小 大于64 的时候,那么会把原来的链表转换成红黑树。 - 为什么会是红黑树?
红黑树 的查询、删除和添加时间复杂度是
O
(
l
o
g
2
n
)
O(log_2n)
O(log2?n)链表 的查询和删除的时间复杂度为
O
(
n
)
O(n)
O(n),插入为:
O
(
1
)
O(1)
O(1) - 为什么是红黑树而不是AVL平衡树?
- 红黑树和AVL树都是常见的平衡二叉树,它们的查找,删除,修改的时间复杂度都是
O
(
l
o
g
n
)
O(log n)
O(logn)
- AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树
- 红黑树更适合插入修改密集型任务
- 两个都是O(logN)查找,但是平衡二叉树可能需要
$O(logN$) 旋转,而红黑树需要最多两次 旋转使其达到平衡(尽可能需要检查O(logN)节点以确定旋转的位置),旋转本身是O(1)操作,因为只需要移动指针。 - 由头插法改为了
尾插法 。
|