总结Java中的HashMap

一、HashMap的定义

HashMap是存放key,value键值对的数据结构,是由数组和链表(红黑树)组成的。

二、HashMap的put实现过程

  • 计算Hash值,找到存放的节点,位置为hash%length。由于HashMap的长度总是2的N次方,所以位置可以由hash & (length-1)来计算。
  • 如果存放的节点没有值,就在该位置上生成一个新的节点(Entry或者Node),如果有值了,则需要分情况讨论。
    • jdk1.7:先判断是否需要扩容,不扩容就生成Entry对象,使用头插法添加到链表。
    • jdk1.8:
      • 先判断节点的类型是链表还是红黑树。
        • 如果是红黑树,会将该Node添加到红黑树中。或者存在相同的key就更新。
        • 如果是链表,使用尾插法添加到链表。或者存在相同的key就更新。如果插入以后当前的结点个数大于等于8,则将该链表转换为红黑树。
      • 插入完Node之后,会判断是否元素个数达到阈值,达到就进行扩容。

三、HashMap是怎么扩容的

  • 生成原始容量*2的新数组
  • 遍历老数组的每一个元素
  • 重新计算各元素的下标转移到新数组
    • jdk1.7: 直接将元素添加到新数组
    • jdk1.8: 先判断节点是链表还是红黑树
      • 如果是链表,记录lo和hi两个链表,分别表示新位置在原处的和新位置不在原处的(新位置=旧位置+旧容量),然后分别修改新数组位置的节点。
      • 如果是红黑树,记录lo和hi两个树,也是表示新位置在原处和不在原处的,并且记录每个树的个数,如果最后的个数<=6,就将树转为为链表,最后修改新数组位置的结点。

四、HashMap为什么1.7用头插法,1.8改为尾插法

jdk1.7中插入的方式是头插法,Java1.8改成了尾插法。

  • 因为多线程时,头插法在HashMap需要扩容的时候会造成环状链表,尾插法可以保证原有的顺序,所以之后使用尾插法了。
  • 造成环状链表的原因是:
    • 假设某个位置的链表有A、B两个节点,当第一个线程记录了当前节点A和next节点B就被阻塞了。
    • 第二个线程对HashMap进行扩容,扩容结束后变成了B->A。
    • 第一个线程继续进行扩容,先插入A、再插入B,由于B的next节点是A(本来是null),所以继续循环又插入A。这时A的next是B,B的next是A,造成了循环链表。

五、多线程的情况

虽然jdk1.8之后改为尾插法不会造成环状链表了,但这个方法并不是一个线程安全的类,没有给put/get加锁,会有并发问题。
HashTable和ConcurrentHashMap是线程安全的。

(一)HashTable和HashMap的区别
  1. 实现方式
    Hashtable继承的是Dictionary类,HashMap继承的是AbstractMap类
  2. 线程安全
    HashTable是线程安全的,对数据操作的所有方法都上锁(synchronized),所以效率较低
  3. 迭代器
    HashTable是fail-safe,HashMap是fail-fast。
  4. null
    HashTable不允许键或值为null,HashMap可以。HashTable put空值时会直接抛空指针异常,但是HashMap做了特殊处理,会认为hashcode=0。
    因为Hashtable使用的是安全失败机制,读到的数据不一定是最新数据。如果使用null值,无法判断对应的key是不存在还是为空。
  5. 初始化容量
    HashTable为11,HashMap为16
  6. 扩容机制
    Hashtable翻倍+1,HashMap翻倍
(二)ConcurrentHashMap

在这里插入图片描述

jdk1.7
  • 1.7采用分段锁,数据结构是Segment数组+HashEntry数组
  • Segment继承了ReentrantLock,一个Segment对象可能守护多个HashEntry,HashEntry的next、value由volatile修饰。
  • 由于只锁住了数组的一个位置,每当一个线程访问Segment时,不会影响其它位置,并发性更好。
  • 在进行put时,先自旋获取锁;如果重试次数达到阈值,就改为阻塞锁,保证能获取成功。
  • 在进行get时,由于HashEntry的Value是由volatile修饰,保证了内存可见性,获得的是最新值。这个过程不需要加锁。
jdk1.8
  • 1.8抛弃了原有的Segment分段锁,采用CAS+synchronized保证并发安全
  • HashEntry改为Node,next和value仍然由volatile修饰
  • 在进行put时
    • 如果当前位置为空,使用CAS尝试写入,失败则自旋保证成功
    • 如果当前位置需要扩容,则扩容
    • 都不满足,则利用sychronized锁写入数据(锁的是当前位置的第一个Node)
    • 如果链表长度大于阈值,则转换为红黑树。
  • 在进行get时,没有加锁,也是用volatile保证可见性,直接返回。

总的来说:

  • 锁的粒度更小,1.7锁住多个HashEntry的Segment,1.8锁住单个HashEntry(Node)
  • 1.8用CAS+sychronized,更轻量,特别是后者经过了锁升级。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值