首先来说一说HashMap的结构,在jdk1.8之前是数组加链表,为什么是数组加链表,因为这样可以有效解决hash碰撞问题,java中就使用“拉链法”来解决hash碰撞问题。
一.hash碰撞:简单来说就是
1.HashMap中的Key和对key做一个hashcode()的计算后得到的它在bucket数组中的位置相同时,注意,准确的说是hash计算后得到数组中的位置,(当然,表达成hashcode值相同时也对,但是根本是得到在数组中的位置)也就是数组下标相同时,产生hash碰撞,这个时候就会替换原来的值。(这也就是为什么HashMap的Key是唯一的)
2.那如果只是hash后的值相同,而key不同的话,就会用"拉链法"将要存的放在后续链表中。
好,那么如果只是使用数组加链表的形式,如果产生hash碰撞将元素链到链表中时,也不能保证完全分布均匀,如果一个链表长度过长,那对应的查找元素的时间就会增加,所以jdk1.8之后就加入了红黑树,当链表长度达到阈值8的时候就会将链表转换成红黑树,查找的时间复杂度就会降低。如果说链表中有n个元素,链表的遍历时间复杂度就是O(n),红黑树查找的时间复杂度就是O(logn)。那么查找的时间复杂度就会降低。
二.jdk1.8中对hash算法和寻址算法的优化
之前在对key进行hash(key)计算时,采用的是取模运算,而优化后采用的是寻址算法,即:(n-1) & hash 『n为数组长度』,为什么要使用寻址算法呢?首先 「hash & (n-1)」 效果跟 hash 对 n 取模的效果是一样的, 但是『&』与运算的性能要优于 hash 对 n 取模。
三.那么HashMap中是怎么进行put操作的呢?
首先hash(key),得到hash值在通过『&』与运算((与Key.hashCode的高16位做异或运算)),得到了数组存储位置bucket。
散列表为空,就初始化散列表。
没有发生碰撞就直接添加到相应位置。
发生碰撞:
1:若key地址相同或者equals后内容相同,则替换旧值
2:如果是红黑树结构,就调用树的插入方法
3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
如果桶满了大于阀值,则resize进行扩容
四.hashmap扩容
当数组满之后会进行扩容,扩容为原来的两倍。
扩容后还要重新对每个hash值进行寻址,也就是用每个hash值跟新的数组长度 n-1 进行『&』与运算操作。
补充:扩容之后的与运算可能会导致之前的发生hash冲突的元素不再发生冲突。
HashMap 在多线程是不安全的,为啥呢?或为什么说HashMap可能会导致死循环?
这个过程就是发生在扩容阶段,在jdk1.7之前,hashmap在扩容2倍,得到新的数组,会将原来的元素重新hash放入到新的扩容后的数组里面去,因为采用的是头插法,头插法就是总是把新增结点插在头部,会造成链表翻转形成闭环,也就是形成死循环。所以会导致不安全
jdk1.8之后虽然是尾插法,但是两个线程或多个线程同时插入元素,如果元素插入同一个位置,有可能导致覆盖。