HashMap

HashMap

creative_card

数组和链表

​ 这两种数据结构想必都早已耳熟能详,而今天的主角,HashMap也就是基于这两种结构实现的哈希表结构,当然是以前的主角(JDK1.7)。现如今工作中JDK8普及较广,而在8中也将哈希表结构进行了优化,这个后面再深入了解。

​ 数组的特点:查询和修改效率高、插入和删除效率低、存储空间连续、内存占用大、空间复杂度大

​ 链表的特点:查询和修改效率低、插入和删除效率高、内存利用率高、扩展灵活

​ 哈希表:结合数组和链表的结构特点,好处全都要

HashMap结构

​ 从图中可以看出,哈希表的结构是数组内存链表的形式,链表中前者存Key,后者存Value,末尾指向下节点的Key,无后节点即存null。

​ 在Java7中,向HashMap中存入一个值的过程:

​ 1)通过数值的类型,计算出该值的HashCode(JVM虚拟出来的内存地址通过hash算法,得到的32位整数),将该hash值转换为数组的下标,进而确定了桶的位置。

​ 2)判断该位置的通是否有值,没值就在这个位置添加节点,将值保存进去;有值就开始用Key和链表上的每个Key进行equals比较,找到相同的就替换掉原有值,没找到就在最后追加j节点。

​ 取值的过程就是存值的反转。

​ 注意:若是自定义的类要使用 hashMap 结构,一定要实现 hashcode 方法和 equals 方法,否则在刚才的操作过程中就会引发问题。equals在比较未重写的引用类型的时候,比较的是内存地址,所以通过内存地址计算而来的 hashcode 是一定相同的,而在比较基本数据类型的时候是直接比较值是否相等的。由于 hashcode 会有会哈希冲突的问题,导致不同的地址计算出相同的hash值,所以 hashcode 相同并不能证明对象是相同的,必须用equals。

​ equals 和 == 的区别:equals可以被重写,可以被设计。equals期望被用于值的判断,==期望被用于对象是否一致的判断。他俩判断基本类型是一样的,比较自定义类型时,两者一样。

红黑树

​ 上文中提到,JDK8 对 HashMap 进行了优化,其中部分条件下,链表会转换成红黑树的结构,简要说明下红黑树的一些特性,操作思路。

​ R-B Tree,全称Red-Black Tree,又称‘红黑树’,一种特殊的二叉查找树,有自平衡的特性,每个节点有存储位表示节点的颜色,红或黑。红黑树有一下5种特性: 1)每个节点或者黑色、红色 2)根节点为黑色 3)每个叶子节点为黑色(为空的叶子节点) 4)若某节点为红色,则其子节点必须为黑色 5)从一节点到该节点的子孙节点的所有路径上包含相同数目的黑节点

​ 红黑树的修改操作有删除和新增,这两者都不免涉及到旋转这一行为,原因是在插入或删除后,树的结构可能就不满足红黑树中的特性5,于是要对其进行修正,就是为了让他再次成为一颗红黑树。

左右旋

​ 图中简单展示了左旋和右旋的过程,我将其简单理解为左旋就是向左90%,右旋也是如此。例如图中左旋时,将A节点的右节点提升为自己的父节点,而B节点的左节点代替了B原来的位置,这样就完成了左旋,好像B篡位了一般。右旋则是一个倒置的过程。

​ 在插入的过程中,要先将节点涂红再向内添加,实际面对的情况不同,应对策略也不同。 1、若被插入的是根节点,那就直接涂黑; 2、被插入的父节点是黑色节点,不用操作,原本就是红的; 3、被插入的父节点是红色节点,在这种情况下就有细分的三种情况: 3.1、父节点的兄弟节点即叔叔节点为红,则先将父节点和叔叔节点设为黑,而后将祖父节点变红,最后将祖父节点看作当前节点并向上推演 3.2、叔叔节点为黑、且当前节点为父节点右节点,则以父节点为中心,进行左旋 3.3、叔叔节点为黑,当前节点为左节点,则将父节点变黑、祖父节点变红,以祖父节点为中心,右旋

​ 红黑树插入效率低外,其他的都优于链表,当表的单一链表长度超过8时以及数组长度大于64时,链表的结构会转为红黑树结构,但是如果此时删除红黑树中的节点小于等于6个,那么原有的红黑树又会变回链表。这样设计的好处就是避免当链表变得很长很长,查询效率下降这种情况 红黑树的查询效率类似折半查找,时间复杂度为 O(logn) ,链表则需要遍历全表,复杂度为 O(n) ;由于红黑树是一种近似平衡的二叉查找树,优点就是较为平衡,这还要得益于红黑树特性中的第五条。


​ 总结:红黑树是建立下二叉查找树的基础上演变而来,查询性能很高,主要用来存储有序数据,时间复杂度是 O(lgn) 。Java集合中的TreeMap和TreeSet,C++中STL中的set、map,以及linux中虚拟内存管理都是通过该结构去管理的。JDK8通过引入这一结构,优化原有的链表查询效率问题。上文简要介绍红黑树在插入的过程中,正确的应对姿势和思路,修改操作中的删除就不进行太多的说明,仅仅作为了解HashMap的一些要素知识,管中窥豹下红黑树。

HashMap

​ 基于哈希表Map接口的异步实现,允许null值和null键。此类不保证映射的顺序,且顺序是可变化的。由于HashMap不是同步的,多线程同时访问一个Map,且有一个及以上线程进行了删除或添加操作,则必须保持外部同步(也就是加锁保证操作,非同步指调用即返回成功,后续还在操作中)

​ JDK7中底层实现使用数组加链表,演进到JDK8时,引入了红黑树优化效率。增加元素时,根据key的hashcode计算hash值,通过该值得到数组中的下标,若数组上已有元素,则在该位置以链表或红黑树的形式保存,相同的hashcode保存位置也相同,相同时就会调用equals方法进行比较,若相同则覆盖原值,反之新加入的放链头,先加入的放链尾,没有则直接放到该数组位置上。若数据在链表上均匀分布,查询效率会较高。

​ 当HashMap中的元素越来越多时,Hash冲突的几率也越来越高,hash冲突的几率也越来越高,此时需要对数组扩容。原数组中的全部元素都需要重新计算位置,并放进新的数组中,这是最消耗性能的,也是提供的resize方法。扩展的时机: 当元素个数超过 容量*负载因子数数量时,会进行扩容操作。默认状态下,容量是16,负载因子为0.75,计算为12,即超过12个元素就会进行扩容,扩容的大小是原来的一倍,即由16扩容为32。这个操作非常消耗性能,若是能预知集合大小,先设置好是不错的选择。

oumei_zhuming_kehuan_yingshi-002

​ 负载因子:loadFactor衡量的是散列表的空间使用程度,负载因子越大空间的装填度越高,但查询效率反而变低,但负载因子较小则是对空间的浪费,所以负载因子可以理解为是时间和效率权衡后结果的具象化表现,默认设置为0.75。当容器中元素的实际个数超过了 容量*负载因子,则需要进行扩容操作,以此降低实际的负载因子,对空间和时间的一种平衡。

​ Fail-Fast机制:HashMap不是线程安全的,若在用迭代器过程中其他线程修改了Map,就会出现concurrentmodificationException,也就是Fail-Fast策略。实现是通过类似乐观锁的方式,在内部维护了一个内存可见的整型变量modCount,用volatile修饰,每次修改集合都会++这个值,在迭代器进行迭代时也会传递给它,在迭代过程中会判断该值是否相等,否则抛出异常。注意:这个Fail-Fast机制是不能完全保证的,在面对多线程并发修改的时候,特别是非同步的并发修改,不能做到决绝的保证,只能进最大努力抛出,因此不要依赖该机制的异常进行处理,而仅仅是作为检测程序来使用

​ 遍历HashMap时,尽量使用entrySet集合遍历,不要用KeySet,前者的性能相对高些(后者基于前者)

Hash冲突:两个key可能得到相同的hash值

  • 解决方案:开放定址法、再哈希法、链地址法、建立公共溢出区
  • 开放定址法:当关键字的哈希值p冲突,则根据p再搞个哈希值p1,若冲突了再继续,直到不冲突
  • 再哈希法:同时构造多个不同的hash方法,一个冲突了换另一个
  • 链地址法:将同地址的值组成一个同义词链,插入、删除和查找都在同链中
  • 建立公共溢出区:将哈希表分为基本表和溢出表两个表,凡是冲突的元素填入溢出表中

ConcurrentHashMap

​ ConcurrentHashMap和HashMap很类似,JDK7中用数组加链表,JDK8中用数组加链表引入红黑树,不同点是核心数据如value,链表都是volatile修饰,保证了内存可见性。其采用分段锁技术,其中segment继承自ReentranLock,不像HashTable那样不论put、get都要同步处理,理论上concurrentHashMap能支持到currencyLevel(数组数量)的并发数,并发占用一个segment不会影响其他segment

​ put方法:虽然entry用volatile修饰,但不保证原子性,需要加锁。首先获取锁,获取失败会不断自璇获取,达到MAX_SCAN_RETRIES数量会由自旋锁转为阻塞锁。get方法:比较简单,直接获取就行,由于是volatile修饰,保证是最新的值。

​ JDK8中引入了红黑树解决链表效率问题,同时抛弃分段锁,转而用CAS+synchronized保证并发

​ 上图是put的过程:1)计算hash值确定位置 2)初始化数组 3)若位置为空值则CAS写入,失败用自旋保证成功 4)判断扩容与否

5)都不满足,用synchronized保证写入数据 6)数量超标转为红黑树;get过程:通过hashcode寻址,在桶上就直接返回,否则按照红黑树或链表的结构去查

参考文章

Java哈希值Hashcode理解

HashMap底层实现原理解析

红黑树原理和算法介绍

关于HashMap何时会将链表转换成红黑树的问题

HashMap底层原理

HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!

posted @ 2022-04-06 21:39  lifelikeplay  阅读(32)  评论(0编辑  收藏  举报