HashMap
1.1 HashMap 的数据结构?
哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。
1.2 HashMap 的动态扩容
当HashMap中的元素个数超过临界值时会触发动态扩容(threashold(临界值)=capacity (容量大小)* loadfactor(装载因子)
),扩容 resize
为 2n :
-
table 数组大小是由
capacity
这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30; -
loadFactor
是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75(概率泊松分布),比如table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容; -
扩容时,调用
resize()
方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold) -
如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
1.3 Hash实现方法
JDK 1.8 中,是通过 hashCode()
的高 16 位异或低 16 位实现的:``(h = k.hashCode()) ^ (h >>> 16)`,主要是从速度,功效和质量来考虑的,通过位移运算来提升Hash值的散列度,降低Hash冲突的概率从而减少系统的开销
1.4 如何解决Hash冲突
HashMap引入了链式寻址法来解决Hash冲突的问题,用尾插法将元素放在链表的尾部:如果链表长度超过阀值(TREEIFY THRESHOLD==8 && SIZE >=64
),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表。
解决Hash冲突一般有三种解决方法:
- 再Hash法:就是如果某个hash函数产生了冲突,再用另外一个Hash进行计算,比如布隆过滤器就采用了这种方法
- 开放寻址法:就是直接从冲突的数组位置往下寻找一个空的数组下标进行数据存储,这个在TreadLocal里面有使用到
- 建立公共溢出区,也就是把存在冲突的key统一放在一个公共溢出区里面
ConcurrentHashMap
HashMap在并发操作中会产生并发安全问题,如死循环问题、数据覆盖问题等。因此在并发操作时一般会采用ConcurrentHashMap来解决,ConcurrentHashMap 类是 Java并发包java.util.concurrent
中提供的一个线程安全且高效的 HashMap 实现
JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。
下面是put操作的源码:
- 根据 key 计算出 hashcode,然后开始遍历 table;
- 判断容器是否为空,如果为空就会使用Volatile+CAS来初始化,
- 如果不为空就会根据存储的元素计算该位置是否为空,如果根据存储元素的计算结果为空,就会利用CAS来设计该节点
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果存储元素的计算结果不为空,就会使用 synchronized 加锁来进行实现,然后遍历并修改桶中的数据
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。