你真的了解【HashMap】么?-一

Q1:HashMap的内部数据结构

JDK1.8版本之前是数组+链表,1.8版本之后是数组 + 链表+红黑树

数据结构图:

Q2:HashMap初始容量大小

如果 new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为第一个 大于k的 2的整数次方,例如 如果传10,大小为16

//传入初始大小后 计算HashMap容量的方法
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;
}

Q3:HashMap的哈希函数

 hash函数是先拿到 key 的hashcode,是一个32位的int值,然后让hashcode的高16位和低16位进行异或操作

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

为什么要这样设计?

hashcode右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性,而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

这个也叫扰动函数(或者扰动计算),这么设计有两点原因:

  1. 降低hash碰撞,越分散越好;
  2. 因为这是高频操作, 因此采用位运算,更加高效;

Q4:如何获取数据存储位置下标

对数组的长度取模运算,得到的余数才能用来访问数组下标,源码中模运算就是把散列值(hash)和 数组长度-1 (length-1) 做一个"与"操作,位运算比取余%运算要快

数组下标 i = h (length-1)

这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”,“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。

以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111,和某散列值做“与”操作如下,结果就是截取了最低的四位值,

  10100101 11000100 00100101
& 00000000 00000000 00001111 ---------------------------------- 00000000 00000000 00000101 //高位全部归零,只保留末四位

Q5:HashMap数据插入流程图

1、判断数组是否为空,为空进行初始化;
2、不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
4、存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
5、如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
6、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
7、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍(先插入最后扩容的缺点:有可能扩容之后后续就没有数据插入了,浪费,扩容很消耗性能)。


Q6:JDK1.8做了哪些优化及原因

1、数组+链表改成了数组+链表或红黑树;

原因:防止发生hash冲突,链表长度过长,查询效率低,修改为红黑树后将时间复杂度由O(n)降为O(logn),


2、链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,JDK1.7将新元素放到数组中,原始节点作为新节点的后继节点,JDK1.8遍历链表,将元素放置到链表的最后;

原因:1.7头插法扩容时,会使链表发生反转,多线程环境下会产生环


3、扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;

原因:由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,怎么理解呢?

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。

因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的两种情况:

第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)


4、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

Q7:HashMap线程安全么

不安全,在多线程环境下,JDK1.7 会产生死循环、数据丢失、数据覆盖的问题,JDK1.8 中会有数据覆盖的问题

HashTable、Collections.synchronizedMap、ConcurrentHashMap 是线程安全的

 1、HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大;

2、Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;

3、ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高

—————————————————————————————

感谢  https://blog.csdn.net/zhengwangzw/article/details/104889549  

 

posted on 2020-07-24 17:09  无言寒冰  阅读(183)  评论(0编辑  收藏  举报