HashMap


HashMap实现原理

HashMap概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并且允许使用null值和null键。此类不保证映射的顺序,特别是不找正顺序是永恒不变的。

HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 1.7 和 1.8

jdk1.8相对于1.7底层实现发生了一些改变。1.8主要优化减少了Hash冲突 ,提高哈希表的存、取效率。

底层数据结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,转为红黑树)。
JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表。
1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中。
当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部;而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部。
1.7中新增节点采用头插法,1.8中新增节点采用尾插法。这也是为什么1.8不容易出现环型链表的原因。
1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。
1.8rehash时保证原链表的顺序,而1.7中rehash时有可能改变链表的顺序(头插法导致)。
在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。

为什么不直接使用红黑树?

红黑树的五大特点:

1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。

红黑树和平衡二叉树

在这里插入图片描述

HashMap的 put

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpParhTy-1676726689304)(E:\学习\图片\image-20230218205920407.png)]

需要注意jdk1.8中对hashMap的实现做了优化,当链表中的节点数据超过八之后,该立案表就会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

  • 首先通过hash()函数方法计算出Key的hash值
  • 判断桶数组是否为空,或者长度是否为0,是则使用resize()方法进行初始化;
  • 根据tab[ i =(n-1) & hash ] (n是数组长度),计算key在数组中对应的下标值,如果该位置没有数据,则使用newNode()方法直接插入当前数据
  • 如果对应下标已经存在数据,则需要分三种情况处理碰撞:
    1. 对应数组当前node节点与新元素hash值相等,且key值也相等,则使用新元素value值覆盖旧元素value值
    2. 对应数组当前node节点是树形节点,则将新元素直接插入到红黑树中
    3. 对应数组当前node节点是链表节点,则遍历此链表到尾部,并将新元素插入到链表尾部。插入成功再根据链表长度与树化阈值的比较判断是否需要进行树化
  • 最后在所有元素处理完毕后,还需判断当前元素大小是否超过扩容阈值,超过则调用resize()方法扩容

hashmap扩容

1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。

由于数组的容量是以2的幂次方扩容的,那么一个Entity在扩容时,新的位置要么在原位置,要么在原长度+原位置的位置。原因是数组长度变为原来的2倍,表现在二进制上就是多了一个高位参与数组下标计算。也就是说,在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据e.hash & (oldCap - 1) == 0判断) 。

这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。

HahMap和HashTable的区别

1 HashMap不是线程安全的

​ HashMap是map接口的子类,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重 复值。HashMap允许null key和null value,而hashtable不允许。

2 HashTable是线程安全。

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。

HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。 HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。 Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。 Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5REquHxp-1676726689305)(E:\学习\图片\image-20230218211511853.png)]

cncurrentHashMap的实现原理

1.7分段锁 ,16个

在JDK8中,ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式。同时,它又采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。底层数据结构的逻辑可以参考HashMap的实现,下面我重点介绍它的线程安全的实现机制。

  1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)。

  2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。

  3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。

  4. 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。

加分回答

ConcurrentHashMap实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是 (数组长度 >>> 3) / CPU核心数。

也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的。多个线程依据硬件的处理能力,平均分摊一部分槽的迁移工作。另外,如果计算出来的迁移数量小于16,则强制将其改为16,这是考虑到目前服务器领域主流的CPU运行速度,每次处理的任务过少,对于CPU的算力也是一种浪费。

为什么要用ConcurrentHashMap?

HashMap线程不安全,而Hashtable是线程安全,但是它使用了synchronized进行方法同步,插入、读取数据都使用了synchronized,当插入数据的时候不能进行读取(相当于把整个Hashtable都锁住了,全表锁),当多线程并发的情况下,都要竞争同一把锁,导致效率极其低下。而在JDK1.5后为了改进Hashtable的痛点,ConcurrentHashMap应运而生。

hashMap遍历

[1、分别遍历Key和Val

[2、使用迭代器Iterator

[3、通过get(不建议因为使用了两次迭代器,效率慢)

[4、 使用默认接口(forEach)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小冻梨♬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值