HashMap 经典问
1. 说一下HashMap的实现原理HashMap 底层主要是数组加链表Java 8 之后链表过长时会转成红黑树。put 的时候会先根据 key 算出来 hash然后通过数组长度算出它应该放在哪个桶里。如果这个桶是空的就直接放进去如果桶里已经有元素就说明发生了哈希冲突这时会逐个比较 hash、 和 equals。如果发现 key 已经存在就覆盖旧 value如果不存在就插到链表或者红黑树中。get 的过程也类似先根据 hash 定位到桶再在桶里面通过 hash 和 equals 找到真正的 key。2. HashMap的jdk1.7和jdk1.8有什么区别JDK 1.7 的 HashMap 底层是数组加链表JDK 1.8 在此基础上增加了红黑树。链表长度达到 8并且数组容量达到 64 时会转成红黑树节点过少时又会退化成链表。另外JDK 1.7 链表插入是头插法多线程扩容可能导致链表成环JDK 1.8 改成尾插法并优化了扩容时节点迁移方式。但两者都不是线程安全的。3. HashMap的put方法的具体流程put 的时候如果底层 table 还没初始化会先调用 resize 做初始化。然后根据 key 计算 hash再通过数组长度定位到具体桶的位置。如果这个桶是空的就直接创建新节点放进去。如果桶不为空会先判断桶里的第一个节点是不是同一个 key判断时会比较 hash、和equals。如果是同一个 key就覆盖旧 value。如果第一个节点不是同一个 key就继续看这个桶的结构。如果是红黑树就走红黑树的插入或覆盖逻辑如果是链表就从头往后遍历找到相同 key 就覆盖没找到就插到链表尾部。新增节点之后如果链表长度达到 8会尝试树化但如果数组长度还小于 64会优先扩容不会直接转红黑树。最后如果这次 put 是新增元素size 会加 1当 size 超过 threshold扩容阈值也就是容量乘负载因子时会触发扩容。4. 讲一讲HashMap的扩容机制HashMap 的扩容主要发生在两个场景第一次 put 时 table 还没初始化会调用 resize 初始化后面每次新增节点后如果 size 超过 threshold也就是容量乘负载因子就会触发扩容。默认初始容量是 16负载因子是 0.75所以默认阈值是 12超过之后容量一般扩成原来的 2 倍。扩容时会创建一个新的数组然后把老数组里的节点迁移过去。如果某个桶里只有一个节点就直接根据hash (newCap - 1)算出新下标放过去。如果桶里是链表JDK 1.8 会利用容量翻倍这个特点把链表拆成两组。判断条件是hash oldCap如果结果是 0节点还留在原下标如果不是 0就移动到原下标加 oldCap 的位置。这样不需要完全重新计算 hash而且还能保持链表的相对顺序。如果桶里是红黑树也会做类似拆分拆分后节点太少可能退化成链表否则继续保持红黑树。5. hashMap的寻址算法HashMap 寻址先通过 key 的hashCode()计算 hashJDK 1.8 会用h ^ (h 16)做扰动让高位也参与低位计算减少冲突。然后用(n - 1) hash 计算数组下标。因为数组长度是 2 的幂所以这个位运算可以代替取模效率更高。6. 为何HashMap的数组长度一定是2的次幂?HashMap 数组长度是 2 的幂主要是为了用(n - 1) hash代替取模计算下标位运算效率更高。而且当 n 是 2 的幂时n - 1的低位全是 1能让下标分布更均匀减少冲突。扩容时也更方便因为容量翻倍后元素只可能留在原位置或者移动到原位置加 oldCap通过hash oldCap就能判断。7. hashmap在1.7情况下的多线程死循环问题JDK 1.7 的 HashMap 底层是数组加链表。扩容时HashMap 会把旧数组中的链表迁移到新数组。JDK 1.7 迁移链表用的是头插法也就是每迁移一个节点都插到新链表的头部所以链表顺序会被反转。比如原来是A - B迁移后可能变成B - A。单线程这样没问题但如果两个线程同时 resize它们可能同时操作同一条链表并且都在修改节点的next指针。一个线程刚保存了下一个节点另一个线程已经把链表迁移并反转了前一个线程恢复执行时再继续改next就可能让节点之间互相指向比如形成A - B - A这种环。链表一旦成环后续get或遍历这个桶时就会一直沿着next走走不到null于是出现死循环。JDK 1.8 改成了尾插法扩容时会保持链表的相对顺序缓解了 1.7 的成环问题