HashMap.put
处理流程
容量2的倍数?元素位置怎么确认的?
最简单的散列函数就是,按容量取模。位运算性能高于取模,但是如果容量不是2的倍数,就会导致数组中有些位置没有机会放置元素;
例如容量为3,二进制位就是 10
,由于第一位为0,位与的结果第一位一直是0,三个位置(00,01,10)中,01
位置没机会放置元素,浪费空间;
容量值0位越多,浪费的空间越多。如果容量值为2的倍数,则容量值减1,二进位全是1,所有位置都有机会放置元素。
hash值计算,低位和高位异或,在容量小的情况下,更加散列,减少冲突:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
位置:(capacity - 1) & hash;
扩容前,
oldCap-1就是0到n-1二进制位都是1;
扩容后,
newCap-1 就是0到n二进制位都是1;
如果hash 的第n位二进制位是0,计算的位置就和扩容前的一样,若为1则位置比扩容前大了一个oldCap,即 新位置=旧位置+oldCap;
所以扩容时,直接用hash & oldCap
来判断,等于0位置不动,等于1则,新位置=旧位置+oldCap,相比于取模性能高。
为什么负载因子是0.75?
负载因子越大,空间利用率越高,冲突机率越大,增加结构复杂性,查找成本高;
负载因子越小,空间利用率越小,冲突率低,查找效率高;
冲突率和空间利用率,0.75是个最佳实践
为什么树化?树化和反树化时机?
红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率。
插入元素后,判断链表长度大于8,就树化;
扩容时,位置上是红黑树结构,有一部分放置旧位置形成一个链表,另一部分放置到新位置形成一个链表,对于两个链表长度大于6,转换成红黑树,否则就以链表结构;
线程不安全的原因
1.在jdk1.7中,在多线程环境下,头插法,扩容时会造成环形链或数据丢失。
2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。