0、前言

  • 本博文部分文字及图片参考自以下三篇文章,其余内容为本人经过思考及总结后所写,仅作为学习分享使用,如有侵权,请联系本人删除,谢谢。

    • 1、什么是HashMap

    • 2、高并发下的HashMap

    • 3、什么是ConcurrentHashMap?

1、HashMap基本原理

  • 众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。

  • HashMap数组每一个元素的初始值都是Null。

  • 对于HashMap,我们最常使用的是两个方法:Get 和 Put。

  • Put方法的原理

    • 调用Put方法的时候发生了什么呢?

    • 比如调用 hashMap.put("apple", 0) ,插入一个Key为“apple"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):

      index =  Hash(“apple”)
      
    • 假定最后计算出的index是2,那么结果如下:

    • 但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:

    • 这时候该怎么办呢?我们可以利用链表来解决。

    • HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可:

    • 需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
  • Get方法的原理

    • 使用Get方法根据Key来查找Value的时候,发生了什么呢?

    • 首先依然会把输入的Key做一次Hash映射,得到对应的index:

      index =  Hash(“apple”)
      
    • 由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:

      • 第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。

      • 第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。

      • 在这里get方法会沿着链表一直往下寻找,直到找到了key为apple的节点。若找到链表最尾端的时候(e.next=null)还找不到的话,则返回null

        • 所以当此处出现双向循环链表的时候,那么程序就会出现死循环,因为e.next永远不会等于null

2、HashMap默认初始长度是多少,为什么?

  • 默认长度是16,并且每次自动扩展或手动初始化时,长度必须是2的幂次方。

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
  • 之前说过,从Key映射到HashMap数组的对应位置,会用到一个Hash函数,如何实现一个尽量均匀分布的Hash函数呢?我们通过利用Key的HashCode值来做某种运算。

  • 通常情况下,Key的HashCode值会是一个比较大的值,但我们HashMap的初始长度只有16,所以我们必须采取某些方法来将这个HashCode值和Map的长度值做一个映射转换

    • 常见的做法就是将Key的HashCode值和Map的长度值进行求模运算,但模运算效率低,为了实现高效的算法,HashMap采用了位运算的算法。

    • 如何进行位运算呢?有如下的公式(Length是HashMap的长度):

      index =  key.hashCode() & (Length - 1)
      
    • 下面我们以“book"的Key来演示整个过程:

      • 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的幂次方,这样才能保证Length-1的二进制形式全是1。因为假如Length-1的值为1000的话,那么其他数和1000进行与运算之后,结果就只有1000或0000这两种情况,这样就会造成大量的冲突,显然不符合Hash算法均匀分布的原则。

3、HashMap的扩展

  • HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。

  • 影响发生Resize的因素有两个:

    • 1、Capacity:HashMap的当前长度。

    • 2、LoadFactor:HashMap负载因子,默认值为0.75f。

  • 衡量HashMap是否进行Resize的条件如下,也就是说当HashMap中存储的数据量超过总量的0.75倍的时候,则认为该HashMap已经超过负载,需要进行Resize:

                      HashMap.Size >= Capacity * LoadFactorHashMap.Size>=Capacity∗LoadFactor
  • Resize的步骤

    • 1、扩容

      • 创建一个新的Entry空数组,长度是原数组的2倍。
    • 2、ReHash

      • 遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

      • 让我们回顾一下Hash公式:

        index =  key.hashCode() & (Length - 1)
        
      • 当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。

      • Resize前的HashMap:

      • Resize后的HashMap:

      • ReHash的Java代码如下:

        /*** Transfers all entries from current table to newTable.*/
        void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);// 头插法,让e.next指向链表中的最后一个节点e.next = newTable[i];// 然后让e成为链表中的第一个节点newTable[i] = e;e = next;}}
        }
        
  • HashMap的扩展在多线程下会造成死循环

    • 如当前HashMap中存在两个元素a和b,其中他们存放的地址冲突,即hash(a) = hash(b) = 0,此时该哈希表的内存图如下:

    • 假如现在有两个线程分别对该HashMap执行put操作,此时HashMap由于容量不够就需要进行扩容了,假设线程1先执行,在执行完Entry<K,V> next = e.next;这一句之后,cpu的时间片切换到了线程2上了,并且线程2顺利地执行完毕,此时我们先看线程2执行完后的内存图是怎样的。

    • 此时又轮到线程1执行了,我们回顾下rehash的代码

      /*** Transfers all entries from current table to newTable.*/
      void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {// 线程1执行完后停在了这里,e=0x001,next=0x009Entry<K,V> next = e.next;// 线程1又执行了// 与之前不同的是,原本是0x009.next = null,现在变成了0x009.next = 0x001if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;}}
      }
      
    • 线程1在while代码块中执行了三次

      • 1、e=0x001,next=0x009,然后newTable[2] = 0x001,0x001.next = null
      • 2、e=0x009,next=0x001,然后newTable[2] = 0x009,0x009.next = 0x001
      • 3、e=0x001,next=null,然后newTable[2] = 0x001,0x001.next = 0x009
    • 最终内存图如下:

    • 此时当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于2的时候,由于位置2带有环形链表,所以程序将会进入死循环!

4、ConcurrentHashMap

  • 由于多线程在操作HashMap时会出现环形链表进而导致死循环的问题,所以此时就必须寻找解决方法。

  • Hashtable或Collections.synchronizedMap均能保证线程的安全性,但两者都使用了带有阻塞的悲观锁,性能不高。

  • 在并发环境下,ConcurrentHashMap能到兼顾线程的安全性以及运行的效率,替代了Hashtable。

  • ConcurrentHashMap通过使用Segment的方式来减少悲观锁的产生。

    • 其原理有点类似于jvm堆内存分配对象时所使用的本地线程分配缓冲(TLAB),每个线程有一个自己专属的区域,各个线程在自己的区域中执行代码,互不干扰。

    • ConcurrentHashMap则可以看成一个二级哈希表,首先其维护的哈希表中存储的均为Segment对象,而各个Segment对象中同时也维护了一个哈希表,哈希表里面存放的才是真正我们要用的entry对象。

    • 如果两个线程同时操作两个Segment中的两个哈希表,那么自然也就不会出现线程安全性问题了,两者可以同时执行。

    • 这样子设计之后,当我们put进一个元素时,就需要进行两次hash值的获取,第一次先获取key所对应的Segment的位置,第二次再获取key在Segment中所对应entry对象的真正位置。

    • 当然,AB线程也有可能同时操作到同一个Segment,为了保障线程的安全性问题,Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞(由于只对写操作上锁,所以并发读或一个线程读一个线程写的情况并不会被阻塞)。

    • 由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度(降低了线程阻塞的可能性),让并发操作效率更高。

  • 总结ConcurrentHashMap的get步骤和put步骤如下:

    • get

      • 1、为输入的Key做Hash运算,得到hash值。

      • 2、通过hash值,定位到对应的Segment对象

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

    • put

      • 1、为输入的Key做Hash运算,得到hash值。

      • 2、通过hash值,定位到对应的Segment对象

      • 3、获取可重入锁

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

      • 5、插入或覆盖HashEntry对象。

      • 6、释放锁。

  • ConcurrentHashMap如何保障size()方法数据的一致性?

    • ConcurrentHashMap中的size方法是通过将各个Segment内部的元素数量汇总起来从而得出ConcurrentHashMap元素的总数量的。

    • 假如size方法在统计完Segment1之后,准备统计Segment2的数量时,另一个线程往Segment1插入了一个元素,同时比size方法更先运行完毕。那么在size方法运行完成之后,所得出的数量值就会比map的总数量就会少了一个。那么ConcurrentHashMap是如何保证size方法数据的一致性的呢?

    • ConcurrentHashMap是使用了类似于CAS乐观锁的思想来保证size方法统计时不会出现问题,其步骤如下:

      • 1、遍历所有的Segment,把所有Segment的修改次数累加起来(我们在看集合源代码的时候经常会看到modCount++的这个操作,其实modCount变量就是用来统计当前集合修改次数用的)。

      • 2、在第一步遍历的时候,同时把Segment中内部的元素数量累加起来,得到size值。

      • 3、再一次统计所有Segment修改次数的总和。

      • 4、判断所有Segment的总修改次数是否大于我们第一步所统计的修改次数。如果大于,说明统计过程中有修改,重新统计(跳回第一步),记录尝试次数+1;如果不是。说明没有修改,统计结束。

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

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

      • 7、释放锁,统计结束。

HashMap及ConcurrentHashMap基本原理概述相关推荐

  1. Java之HashMap系列--ConcurrentHashMap的原理

    原文网址:Java之HashMap系列--ConcurrentHashMap的原理_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java中的ConcurrentHashMap的原理. JDK7与J ...

  2. Docker基本原理概述

    Docker基本原理概述 Docker是一个用于开发,交付和运行应用程序的开放平台.Docker能够将应用程序与基础架构分开,从而可以快速交付软件.借助Docker,可以以与管理应用程序相同的方式来管 ...

  3. Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的

    多选 Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的:答案在文末 A. Hashtable的K ...

  4. Java7/8 中的 HashMap 和 ConcurrentHashMap

    Java7 HashMap  数组+链表 Java7 ConcurrentHashMap   Segment数组+HashEntry数组链表+ReenTrantLock分段锁 Java8 HashMa ...

  5. Java 7:HashMap与ConcurrentHashMap

    从我过去有关性能的文章和HashMap案例研究中可能已经看到,Java线程安全性问题可以很轻松地使Java EE应用程序和Java EE容器崩溃. 在对Java EE性能问题进行故障排除时,我观察到的 ...

  6. HashMap与ConcurrentHashMap的测试报告

    日期:2008-9-10 测试平台: CPU:Intel Pentium(R) 4 CPU 3.06G 内存:4G 操作系统:window server 2003 一.HashMap与Concurre ...

  7. java+线程安全的hash,多线程下HashMap安全问题-ConcurrentHashMap解析

    Java1.5 引入了 java.util.concurrent 包,其中 Collection 类的实现允许在运行过程中修改集合对象.实际上, Java 的集合框架是[迭代器设计模式]的一个很好的实 ...

  8. HashMap、ConcurrentHashMap原理分析

    集合(Collection)是编程中常用的数据结构,而并发也是服务器端编程常用的技术之一,并发总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存 ...

  9. HashMap与ConcurrentHashMap万字源码分析

    HashMap与ConcurrentHashMap`源码解析 JDK版本:1.7 & 1.8 ​ 开发中常见的数据结构有三种: 1.数组结构:存储区间连续.内存占用严重.空间复杂度大 优点:因 ...

最新文章

  1. 苹果手机4g网速慢怎么办_2020 年双十一建议学生党买 4G 苹果手机吗?
  2. PHP如何将表单提交给自己
  3. 比特位计数—leetcode338
  4. SAP Cloud for Customer的Calculated field字段
  5. 微信小程序 悬浮按钮
  6. 提高C++程序运行效率的10个简单方法
  7. windows下将多个文件里面的内容合并成一个一个文件
  8. JAVA处理模型的步骤,java-处理模型中条件字段的最佳方法
  9. Linux系统管理--Centos6服务管理
  10. 航空系统c语言课程设计报告,c语言课程设计报告_航空订票系统西安郵電學院.doc...
  11. 微弱信号检测_第二版-高晋占
  12. 售前是做什么的?需要具备什么能力?
  13. vx开发|JSON配置文件
  14. 【LearnOpenGL】-PBR材质
  15. java.lang.RuntimeException: Method w in android.util.Log not mocked Androidstudio单元测试配置build.gradle
  16. 【掌上齐齐哈尔】市民网上换领驾驶证方便快捷
  17. audition cc变声插件_Adobe Audition CC怎么安装插件?
  18. c++数独游戏2.0
  19. Linux环境下安装Oracle 11g R2完整图文教程
  20. 快速排序 php内存超限,数据结构与算法设计

热门文章

  1. 突然!华为P30 Pro真机上手视频曝光:屏幕指纹解锁秒开
  2. 苹果最新专利曝光:苹果可能正研发可折叠iPhone
  3. SQL COUNT() 语法总结及用法【原创】
  4. the android sdk location cannot be at the filesystem root
  5. Php 取出session中的值,获取php值
  6. vue 给checkbox 赋值_浅谈vue中关于checkbox数据绑定v-model指令的个人理解
  7. centos php 开启libgdgd_CentOS6.5安装Nginx1+MySQL5+PHP5
  8. vue-cli目录结构介绍
  9. 【MyBatis】MyBatis Order By 字段动态动态排序
  10. 【Elasticsearch】 Elasticsearch对外提供分词服务实践