Java基础

HashMap与ConcurrentHashMap

HashMapJDK7采用数组+链表的数据结构,JDK8采用数组+链表+红黑树,扩容因子为0.75,是JDK团队根据泊松分布计算得来

put方法:

首先获取Key的hashCode,然后再使用扰动函数进行混淆hashCode的高位与低位(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),使得能在数组中分布的更加分散



然后会判断数组是否已经初始化了,如果没有则初始化数组,数组长度会设置为比指定长度大的最近的2次幂数,比如指定30,则初始化的数组长度会为32,指定17也会取32,指定15则取16
如果没有指定长度默认初始化为16
数组初始化完成后,则根据hash值和数组长度做与运算,得出最终所在的数组下标位置(n - 1) & hash
接着会判断计算出的数组下标位置是否已经有元素存在了,没有则初始化Node节点
需要注意的是数组扩容也是调用的resize()方法




如果已经有元素存在,则判断当前已存在的元素的key值是否与put进来的key相等,如果相等则会把对应value替换成put进来的value

中间代码省略



如果key不相同,则判断旧的Node节点是否已经变为TreeNode节点,在HashMap中,发生hash冲突是使用的拉链法处理,就是使冲突部分的元素形成一个链表来维护
如果链表的层级过深,则会导致查询性能下降,所以在JDK8中增加了红黑树来解决链表过深的问题, HashMap根据阈值和数组长度进行判断,链表是否需要转换为红黑树,阈值设定的是8static final int TREEIFY_THRESHOLD = 8;, 数组长度要达到64



如果不是树节点,则进行链表的插入操作
判断头节点的next的值是否为null,是的话,就创建新节点与其关联,接着判断是否达到了转为红黑树的层级深度,达到了则执行链表转红黑树结构逻辑
如果头节点的next的值不为null则判断是否key值一样,一样的话就退出循环在外层做旧值替换,没有的话则继续遍历该链表



扩容源码待补充.
总体来说就是没初始化的话,就先初始化数组
初始化过的则判断是否达到了扩容的阈值,数组长度*扩容因子
扩容大小为原来数组的2倍(左移一位)
然后再对每个元素进行一次hash计算,然后将按照计算的结果重新设置进新的数组位置中
如果是红黑树节点则根据红黑树的方式调整

get方法:

比较简单,就是计算hash后,从对应数组的位置,遍历链表或红黑树取key相等的元素


ConcurrentHashMap

整体流程与hashMap基本一致,计算完hash后,使用一个无限循环进行具体操作,首先也是判断是否需要初始化数组,执行完后开始下一次循环, 再判断头节点是否为null
为null的话就使用cas的方式进行创建Node节点,需要注意的是,多个线程进行cas时,当CAS返回失败时,则会重头开始判断,因为最外层有个无限循环,这样保证了线程安全
不为null则使用synchronized进行对头节点加锁,然后处理插入的动作,具体步骤与HashMap大同小异


比较***钻的问题

  • 为什么扩容因子是0.75?
    因为JDK团队使用泊松分布算法经过大量的实验得出的
  • 为什么数组长度要为2的N次幂?
    为了计算方面的性能,使用位运算性能更高,并且在扩容时只需要看当前数组长度的高一位来看当前元素是否需要移动
  • HashMap如何保证hash算法能够更均匀的分散在数组中?
    JDK团队经过大量实验,使用了扰动函数,key的hash值的二进制的高位与低位混淆
  • ConcurrentHashMap1.8为什么使用CAS+Synchronize
    因为1.7的分段锁设计,每个Segment都继承了ReentrantLock,这个锁是基于AQS实现的,每次竞争不到锁后则会进入等待队列阻塞,而使用Synchronize的话,基于Synchronize的锁消除,偏向锁,轻量级锁的特性,性能已经与ReentrantLock平齐,甚至在某些情况是优于ReentrantLock的, 而且使用Synchronize更方便于后续的JVM级别的优化。况且使用Synchronize锁的是数组的头节点,不像Segment锁的是一段数据,并发度也增高了
posted @ 2022-02-18 14:20  和蔼的马叔叔  阅读(24)  评论(0编辑  收藏  举报