初始化
- 参考
- 例子:
// 假设我们要存放1000个元素
Map map = new HashMap(1000);
for(int i = 0; i < 1000; i++) {
map.put(i, null);
}
// 在初始化Map时,hashmap会计算出table大小应该为1024,但是1024*0.75 = 768
// 也就是说在put第768个时就会触发扩容,导致rehash
// 所以我们应该使用 Map map = new HashMap((int)(1000 / 0.75) + 1);
// 这样初始化时table的大小为2048,2048*0.75>1000,所以我们把1000个元素都put进去也不会触发rehash
- 关键成员变量
- table:hash 表
最开始时为null,当第一次put后才会初始化
- threshold:阈值
除了初始化时为2的次幂,其余时候都为table.length*loadFactor
- loadFactor:加载因子
- table:hash 表
- 初始化流程
- 当调
new HashMap(1000)
时,只是会指定阈值且阈值是 2 的次幂,和扩容因子 - 此时 hash 表还是个 null
- 当第一次 put 后才会初始化 hash 表,并且把阈值变成 0.75
- 当调
// 初始化HashMap对象时,并没有初始化table
public HashMap(int initialCapacity, float loadFactor) {
......
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 第一次put数据时,table为null,调用resize()
public V put(K key, V value) {
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)
n = (tab = resize()).length;
......
}
// resize函数中,初始化了table,并且修改了threshold的值为 newCap * loadFactor
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
......
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
......
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
......
}
多线程 put 时导致成环
- 参考
- JDK1.8 之前的 resize
void resize(int newcapacity) { ... Entry[] newTable = new Entry[newCapacity]; transfer(newTable); ... } void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry e = src[j]; if (e != null) { src[j] = null; do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
在 jdk1.7 及之前的版本中,解决 hash 冲突使用的方式是插入链表,并且是头插法,头插法的代码下面有
hash 桶的头结点是真实的节点,不是哑结点
- 两个线程 put 造成 resize 死循环的场景:
- 假设有两个线程都在插入数据而且都同时触发扩容机制,扩容时又恰好把新数据都放在一个桶
假设为hash桶2
- 假设
hash桶2
的原链表为 A->B->C - 首先线程 2 只执行了很少一部分代码
一个 while 循环都还没来得及完成,刚把 e.next = newTable[i]执行完
,此时 e:A,next:B - 然后线程 1 执行完毕了 resize,此时链表关系变为 C->B->A
- 线程 2 继续执行
- 将上一次循环执行完毕后,链表关系为 newtable[i]:A,e:B
- 接着执行第 2 次 while 循环,链表关系为 newtable[i]:B->A,e:A
如果线程 1 没执行,e 应该是 C,但是线程 1 已经把 B 的 next 指向了 A
- 接着执行第 3 次 while 循环,链表关系为 newtable[i]:A<->B,e:null
死循环出现
- 因为 e 为 null,循环结束
- put 虽然发生了逻辑错误,但是好歹能够执行完毕,但是当 get 的时候,就会因为 A<->B 这个循环链导致无限循环,卡死
- 假设有两个线程都在插入数据而且都同时触发扩容机制,扩容时又恰好把新数据都放在一个桶
头插法
public class Foo {
public static void main(String[] args) {
Node[] table = new Node[5];
int index = 2;
Node node = new Node("a");
node.next = table[index];
table[index] = node;
Node b = new Node("b");
b.next = table[index];
table[index] = b;
Node c = new Node("c");
c.next = table[index];
table[index] = c;
System.out.println(table[index]);
// 输出结果为: c->b->a->
}
static class Node{
private String value;
private Node next;
Node(String value) {
this.value = value;
}
@Override
public String toString() {
return value + " ->" + (next != null ? next.toString() : "");
}
}
}
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于