Java7 从HashMap到concurrentHashMap

    Java8中的HashMap和ConcurrentHashMap相较于Java7中的有比较大的改动,最主要是通过引入了红黑树来提升索引效率.

    HashMap是Java中的一个常用的数据结构, 它是一个用来存储key-value键值对的集合, 每一个key-value键值对被称为一个Entry, 这些Entry分散存储在一个数组中,这个数组就是HashMap的主干. HashMap数组元素初始值为Null.

    对于HashMap的常用方法有 Get和Put

   调用hashMap.put('apple', 0), 插入一个key为'apple'的元素, 这个时候利用哈希函数来确定位置(index)

index = Hash('apple')

假设 index = 2  那么结果如下:

   因为数组长度是有限的,所以会出现entry冲突情况, 如下:

这种情况下,链表可以作为解决方案.

HashMap数组的每一个位置不止是一个Entry对象,也是一个链表的头节点, 当新来的Entry映射冲突时以头插法方式插入对应位置链表即可,如下图

 

   当查找数据时,首先将输入key做一次Hash映射,得到对应的index

index = Hash('apple')

由于冲突存在, 所以有可能匹配到多个Entry, 这时就需要顺序查找链表节点,找到那个节点key值为'apple'的Entry.

前面数据插入时选择的头插法原因是认为数据查找时后插入的Entry被查找的可能性更大

    HashMap默认的初始长度是16, 而且如果需要扩容, 扩容后长度必须为2的幂, 初始长度选择16是为了让从Key映射到index的Hash算法使得数据均匀分布

前面  index = Hash(key)

实际 index = HashCode(key) & (Length-1)

例子:

1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

而如果HashMap的长度不是2的幂, 比如10

那重复上面的步骤

另一个HashCode值

再换一个

很明显 ,当长度为10时, 上述三个index就一样,也就是冲突了.所以说,当HashMap长度不是2的幂时, index冲突的概率更高, 这样不符合Hash算法均匀分布的原则

 当HashMap数组长度为2的幂时,只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的

 

接下来讨论, 高并发场景下HashMap工作方式

    HashMap的容量是有限的,当HashMap数组达到一定饱和度时, key映射位置发生冲突的概率就会慢慢增大, 这时候就需要Resize

是否需要Resize由两个因素决定

    1, Capacity , 即HashMap的当前数组长度

    2, LoadFactor , 负载因子,默认为0.75f

HashMap Resize条件为:  HasgMap.size >= Capacity*LoadFactor

第一步(扩容): 创建一个新Entry空数组, 长度为原来的2倍

第二步(ReHash): 遍历原来的Entry数组, 将所有的Entry重新Hash到新数组.

 

    上述流程在单线程下执行没有问题, 但多线程环境下Rehash操作可能会形成链表环形, 这种情况下如果查找一个不存在的key, 且 index = Hash(key)恰好等于环形链表位置,则查找会出现死循环, 所以HashMap是非线程安全的

 

   接下来, ConcurrentHashMap数据结构是可以保证线程安全的,同时实现高性能读写. 其实保证线程安全的还有HashTable和Collections.synchronizedMap, 但是这两者有存在性能问题, 在读写阶段,他们是通过给整个集合加锁来保证线程安全的. 加锁会导致同一时间其他操作阻塞.

    在ConcurrentHashMap中, 存在一个名为Segment的概念. Segment本身就相当于一个HashMap对象, 它包含一个HashEntry数组, 数组中的每一个HashEntry既是一个键值对(Entry), 也是一个链表的头节点.

单个Segment结构如下:

    在ConcurrentHashMap集合中有2^N个Segment保存在一个名为Segments的数组中, 所以ConcurrentHashMap的结构如下:

可以把ConcurrentHashMap理解为一个二级哈希表, 结构这样设计的好处是可以采用锁分段技术segment之间的读写操作互不影响

    ConcurrentHashMap并发读写情况:

(1) 不同Segment的并发写入          ==========> 可以并发执行

(2) 同一个Segment的一写一读      ==========>可以并发执行

(3) 同一个Segment的并发写入   ==========>需要加锁

    ConcurrentHashMap中的每个Segment各自有自己的锁, 在保证线程安全的同事降低了锁的粒度, 让并发效率大大提升

ConcurrentHashMap的Get 和Put方法

Get方法:

    1.为输入的Key做Hash运算,得到index值。

    2.通过index值,定位到对应的Segment对象

    3.再次通过index值,定位到Segment当中数组的具体位置。

Put方法:

    1.为输入的Key做Hash运算,得到index值。

    2.通过index值,定位到对应的Segment对象

    3.获取可重入锁

    4.再次通过index值,定位到Segment当中数组的具体位置。

    5.插入或覆盖HashEntry对象。

    6.释放锁

 

问题: 每个Segment各自有锁, 那ConcurrentHashMap调用size()获取总元素个数时, 如何确定拿到的是正确的数据? 还有, 如果在统计过程中, 已经统计过的Segment瞬间插入新的元素, 如何处理?

 

ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:

    1.遍历所有的Segment。

    2.把Segment的元素数量累加起来。

    3.把Segment的修改次数累加起来。

    4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。

    5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。

    6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

    7.释放锁,统计结束。

 

本文出处: 程序员小灰

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值