HashMap和concurrentHashMap解析

HashMap1.7

HashMap是非线程安全的,不支持并发操作,其实现比较简单。先来看下JDK1.7中HashMap的结构图

HashMap里面是一个数组,数组中的每个元素是一个Entry对象,每个Entry对象next属性指向下一个Entry。构成一个单向链表。Entry类如下

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单向链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry中

}

如图所示,数组table中存放的每个元素是Entry的实例,为单向链表的头结点。

put值过程

  1. 插入第一个元素的时候,初始化Entry数组。
  2. 通过hash运算获取key的hash值,通过hash值找到元素在数组中的位置,遍历该位置上的链表,如果存在重复key,直接覆盖,put方法就结束了。
  3. 如果不存在重复key,就需要将Entry添加到链表中,在将Entry节点添加到链表之前,先判断数组大小是否已经到达了阈值(数组容量*0.75),如果需要扩容,先进行扩容,扩容后数组大小为原来的2倍。扩容的过程,就是用一个新的数组替换原来的数组。然后重新计算hash值,并重新定位元素在数组中的位置。
  4. 将新增的Entry节点插入到数组相应下标位置处链表的表头。

get过程

  • 根据key值计算hash值,根据hash值找到数组下标。从头开始遍历下标位置处的Entry链表,直到找到相等的key。

ConcurrentHashMap1.7

首先来看下一些ConcurrentHashMap1.7的结构图:

ConcurrentHashMap1.7中是一个Segment数组,数组中每个元素上存放的是一个Segment实例,而Segment内部是由数组+链表组成的。即Segment中包含一个HashEntry数组,而HashEntry又是一个链表结构的元素

在ConcurrentHashMap1.7中,Segment通过继承ReentrantLock的方式来加锁。Segment又称之为分段锁。ConcurrentHashMap1.7中的是一个Segment数组,每次加锁的时候,锁住的是一个Segment对象。也就是说每个Segment都是线程安全的,这样就实现了全局的线程安全性。

ConcurrentHashMap1.7中有个并行级别(concurrencyLevel)的概念,并行级别默认值为16,代表的是ConcurrentHashMap有16个Segment,理论上讲可以支持16个线程并发写入并行级别这个值在初始化的时候可以指定值,但是一旦初始化后,就不能改变了。

ConcurrentHashMap在初始化的时候,会对并行级别进行赋值,根据这个map的容量和Segment的数量计算每个Segment可以分配到元素的大小。再对Segment[0]进行初始化。Segment数组中其它位置还是null。

Put值过程

  1. 根据key获取hash值,找到在Segment数组中的下标位置。初始化下标位置的Segment对象,初始化Segment对象这一步可能出现并发安全问题,因为可能会出现多个线程同时初始化Segment数组中同一位置。这里使用CAS操作初始化Segment对象和它内部的HashEntry数组。准备将值写入到Segment中,Segment内部是由数组+链表组成
  2. 在写入Segment之前,先使用tryLock()尝试快速获取该Segment的独占锁。如果tryLock失败,那么在循环中执行tryLock,直到成功获取该Segment的独占锁。
  3. 获取到Segment独占锁后根据key的hash值获取新增元素在Segment内部的HashEntry数组中下标位置。将新值设置为HashEntry链表的表头,如果超过了阈值,需要进行扩容后,重新计算下标位置,再写入值,最好会释放锁。扩容操作是对segment内部的HashEntry数组进行扩容,容量扩大为2倍,将原数组中的链表拆分的新数组中,此时的操作是持有Segment独占锁的,不需要考虑并发问题

get过程

  • 根据hash值找到Segment数组中的下标位置。遍历链表直到找到key值相同的元素。get的过程是没有加锁的。

HashMap1.8

HashMap1.8和HashMap1.7最大的不同在于引入了红黑树,数组+链表+红黑树组成。

在HashMap1.7中,查找元素的时候,首先需要根据hash值定位到数组的下标,再从链表的头节点挨个遍历,一个一个找下去,知道找到符合条件的记录。随着链表的长度的增加,查找起来更耗费时间。为了解决这个问题,JAVA8中引入了红黑树,当链表中的元素到达8个时,会将链表结构转换为红黑树,减少查找的时间。

Put值过程

  1. 插入第一个元素的时候,会执行resize(),对数组进行初始化。
  2. 根据key的hash值找到数组下标的位置,如果该位置上没有元素,初始化node,放到这个位置上就好了。
  3. 如果下标位置上有元素,那么判断该位置上第一个节点是红黑树结构还是链表结构。如果是红黑树结构,插入到红黑树中。如果是链表结构,插入到链表的最后面(注意:hashMap1.7是插入到链表的最前面)。如果插入的node为链表中的第8个,将链表转换成红黑树结构。
  4. 如果插入新的值导致HashMap的大小超过阈值,需要进行扩容。(注意:HashMap1.8中是先插入值再扩容,HashMap1.7中是先扩容再插入值)。将数组扩容2倍再初始化新的数组将链表中的节点,拆成两个链表,放到新数组中。

get过程

  • 根据key的hash值找到对应数组下标,判断是红黑树结构还是链表结构,如果是红黑树结构,使用红黑树的方式查找。如果是链表结构,从头节点开始遍历链表,直到找到key相等的元素。

ConcurrentHashMap1.8

ConcurrentHashMap1.8结构上和HashMap1.8大致相同,因为要保证线程安全,实现上要复杂许多。

Put值过程

  1. 根据key获取hash值,如果数组table为空,对数组进行初始化。找到Hash值对应的下标,找到头节点,如果该位置上没有元素,那么使用CAS操作将新的值放到这个位置上作为头节点就好了,如果CAS失败,说明有并发,进入下一轮循环。
  2. 如果是数组正在扩容,那么多线程协助扩容,完成数据迁移。
  3. 如果key值对应的下标位置有元素,使用Synchronized获取数组该位置头节点的对象监视器,对这个头节点进行加锁。如果是链表结构,将新值插入到链表的最后面。如果是红黑树结构,调用红黑树插入新值的方法。如果是链表插入完成后,还会判断是否需要将链表转成红黑树,这里跟HashMap1.8有一点点不同,HashMap中是链表长度到达阈值8就转换成红黑树,这里转红黑树的条件链表长度到达阈值8且当前数组的长度大于等于64,否则进行数组扩容。这里的转红黑树也要考虑线程安全问题所以依旧是使用Synchronize对头节点做加锁处理。
  4. ConcurrentHashMap1.8的精髓在于它的扩容迁移操作,这里的扩容也是2倍。但是这里的扩容操作支持多线程执行,它将数据迁移的操作拆分成了n个小任务,充分利用并发的特性,线程来了后帮忙一起迁移数据。这里使用CAS+Synchronized的方式来保证每个线程完成属于自己的任务。

get过程

  • 根据hash值找到数组下标位置,如果该位置上的节点正好就是所需要的,直接返回节点的值。如果头节点的hash值小于0说明正在扩容或者是红黑树结构。如果是链表结构从头结点开始遍历链表,找出key值相等的返回结果。

 

 

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值