假设
- hash表数组长度为4
- hash算法是key % size,即哈希桶的索引bucketIndex = key % size
- hash表中现存在3个元素
key:17 bucketIndex = 17 % 4 = 1
key:9 bucketIndex = 9 % 4 = 1
key:3 bucketIndex = 3 % 4 = 3
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;
}
当准备put元素key=5时,buckIndex = 5 % 4 == 1,触发扩容
触发扩容的条件:hash表的元素数量 >= 阈值 && put的元素hash桶不为空
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
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);
}
oldTable的数据迁移到newTable
void transfer(Entry[] newTable, boolean rehash) {//
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
特别备注:本文所有图中都展示的是内层循环while执行一次的状态图,虚线条表示指针修改后的指向
单线程的扩容
扩容时使用头插法会倒置以前的链表元素的顺序
多线程的扩容(循环死链)
假设现在有thread1和thread2两个线程执行到这里
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;// thread1先执行这里,线程挂起
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
线程1挂起之后,线程1的扩容状态,如下图; 为了区分2个线程,用e1,next1代表线程1的指向,线程2同理
线程2和单线程扩容时是同理,因此不多赘述,线程2扩容结束的状态如下图
线程1重新拿回CPU执行权,继续向下执行,内层循环的第一个循环结束后的状态
此时,我们看到图中已经存在循环引用的情况的存在,内层循环的第二个循环结束后的状态
从上述例子,站在线程1的角度,next本应该是指向下一个元素,但是由于线程2的扩容,修改了指向关系,next不是指向下一个元素而是指向了前一个元素,导致无限循环下去。
总结:多线程并发访问情况下,发生循环死链问题的本质是:某个节点的next指向了链表中前一个节点,从而形成死循环