「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。
简介
HashMap是基于Map接口实现的,允许key和value为null值,但是只允许一个key为null。HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,其访问速度很快但是此类不保证映射的顺序。
HashMap不是线程安全的,多个线程进行同时写HashMap的话可能会产生数据不一致的情况。所以在多线程访问HashMap时候需要提供额外的同步机制。
关于HashMap为何不安全,主要是在JDK1.7中,HashMap采用头插法插入元素,因此并发情况下会导致环形链表,产生死循环。虽然在JDK1.8中采用尾插法解决了这个问题但是并发条件下put操作可能会导致前一个key被后一个key覆盖。
计算hash值
Java哈希表底层采用的key的hashCode方法的值结合数组长度进行无符号右移、按位异或、按位与计算出索引。
redis中哈希算法采用以djb hash为基础,俗称“times33”就是不断的乘33的哈希算法,几乎所有的流行的hash map都采用了DJB hash function。
当两个键的hashcode相同时候通过equals比较内容是否相同。如果相同的话新的value覆盖之前value,反之则将新的键值对添加到哈希表中。
存储模式
java哈希表链表节点长度大于8,而且数组长度大于64则用数组+红黑树进行存储。
关于为什么要节点长度大于8的时候链表转成红黑树,主要是因为树的节点的大小是普通节点的两倍,所以节点多的时候才采用红黑树,而少于该值则换成普通的数组,理想状况下,在随机哈希码下,节点的频率服从泊松分布。
还有一点就是红黑树在节点小的时候删除插入还得维护平衡,比起链表来说稍微复杂一点。
扩容
java哈希表扩容是直接扩容为原来的两倍,默认容量为16,其底层哈希函数为 hash&(length-1),length为哈希表容量,且必须是2的n次幂,如果不是2的n次幂则容易发送hash碰撞。
hashmap中的桶只有map值大于64且链表长度大于8的时候才会进行转换成红黑树,不然则进行扩容
扩容的条件:map大小大于负载因子容量的时候进行扩容
java哈希表如果初始化的参数不是2的n次,则其会通过底层位运算将其转换成满足需要参数大小的最小2的n次
hash 3:容量 8
00000011 与 00000111(7)进行与运算,得到00000011
hash2:容量8
00000010 与 00000111与运算得到00000010
如果容量为9则二进制为00001000则进行与运算极其容易发送hash碰撞
位运算&性能要大于取余%,所以采用位运算
例子
n |= n >>> 1
n |= n >>> 2
n |= n >>> 4
n |= n >>> 8
n |= n >>> 16
n = n+1 //通过这些运算可以将一个数变成最小的大于其的2的n次幂
hashCode右移16位是为了降低哈希冲突,因为如果n-1的值是一个很小的值,hashCode且在高位变化大,在低位变化小,就会导致低位很容易产生冲突
hash 扩容resize() 扩容后,不进行重写rehash,而是判断新增的一位在hash上0还是1,如果是0则在原位,是1则原位+扩容的大小
例如:原位为3,hashmap长度为16,扩容到32,且hashCode在16这一位上为1,那么新的位置就是3+16,反之则为3