HashMap底层原理,以及与HashTable,ConCurrentHashMap的区别

HashMap底层原理:

底层数据结构:

JDK1.7

jdk1.7中HashMap底层的数据结构数组+链表;jdk1.8开始添加了红黑树,当链表的长度达到8之后就会转化成红黑树来存储数据;

JDK1.8

HashMap采用数组+链表+红黑树实现。
在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化:
,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
HashMap的键值对是存储在Entry数组里面的,Entry数组中存放一个Entry类的对象,可以理解成一个节点,实际上是一个单向链表结构,它有一个属性为next,可以指向下一个Entry对象,由此组成一个链表结构,而键值对在Entry数组中的下标是通过元素的key的哈希值对数组长度取模得到的(hash(key.hashCode())%len)

HashMap有四个构造方法:
(1)无参构造器;
(2)指定初始容量的一参构造器;
(3)指定初始容量和加载因子的两参构造器;
(4)指定集合转化成HashMap的构造器;

默认初始容量为16;加载因子默认为0.75
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上图我们可以发现数据结构由数组+链表组成,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key.hashCode())%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。
HashMap里面实现一个静态内部类Entry,其重要的属性有 hash,key,value,next。

为什么加入红黑树?

可以将HashMap的时间复杂度由O(N)变为O(logN),提高了效率。
其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,

扩容(resize())负载因子(loadFactor):

扩容:

HashMap的扩容就是当HashMap中的元素超过一个阈值的时候就会进行扩容,这个阈值的大小等于容量(数组长度)加载因子,加载因子默认是0.75,这个值可以进行修改,甚至可以大于一;但是由于这个Java的前辈们经过不断地测试得出的最佳数据,所以一般不对其进行修改,除非特殊情况。
(**当HashMap中的键值对的数目达到了Entry数组的长度
加载因子的时候,就会发生扩容,将数组的容量变为原来的两倍。**)
当使用无参构造器实例化HashMap的时候,内部默认是null,当第一次使用put方法添加数据的时候,会进行第一次扩容,把HashMap的大小扩大为16。
而后面每次元素超过了阈值之后,每次扩容,数组的大小会变成之前的两倍。
影响扩容主要有两个因素:
  Capacity:HashMap当前长度。
  LoadFactor:负载因子,默认值0.75f。
  怎么理解呢,就比如当前的容量大小为100,当你存进第76个的时候,判断发现大于扩容阈值100*0.75=75需要进行resize了,那就进行扩容,但是HashMap的扩容也不是简单的扩大点容量这么简单的。
  分为两步
(1)扩容:创建一个新的Entry空数组,长度是原数组的2倍。
(2)ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

为什么要重新Hash呢,直接复制过去不香么?
是因为长度扩大以后,Hash的规则也随之改变。
比如原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。

加载因子:

下面说下加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。
另外,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方

链表转红黑树的条件:

当链表的长度超过(阈值)8的时候,链表就会转化成红黑树的结构来存储数据。
在这里插入图片描述

HashMap线程不安全的体现:

参考文章

安全性

HashMap不是线程安全的,要想实现线程安全,就要通过集合工具类来实现
Collections.synchronizedMap(new HashMap<>());

HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

HashMap与HashTable的区别:

1.安全性

两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。
Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。
Note:
Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理。

2.null值的存储

HashMap可以使用null作为key,HashMap最多只有一个key值为null,但可以有无数多个value值为null;
而Hashtable则不允许null作为key;
虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事。
注意:HashMap以null作为key时,总是存储在table数组的第一个节点上。且只能使用一次!

3、继承的接口

HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。

4、初始容量与扩容

HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍再+1即:capacity2+1。

5、两者计算hash的方法不同

Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模;
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。

6、性能:

HashMap的性能最好,HashTable的性能是最差(因为它是同步的)

注意:

1)用作key的对象必须实现hashCode和equals方法。
2)不能保证其中的键值对的顺序
3)尽量不要使用可变对象作为它们的key值。

HashMap与ConCurrentHashMap的区别:

**ConcurrentHashMap:**在hashMap的基础上,ConcurrentHashMap将数据分为多个segment(段),默认16个(concurrency level),然后每次操作对一个segment(段)加锁,避免多线程锁的几率,提高并发效率。

数据结构

jdk1.7:
在这里插入图片描述
jdk1.7中采用Segment+HashEntry的方式进行实现,采取分段锁来保证安全性。Segment 扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 类似,是一个数组和链表结构。

jdk1.8:
在这里插入图片描述
DK1.8 的实现已经摒弃了 Segment 的概念,而是直接用Node 数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized 和 CAS来操作,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

HashMap与ConcurrenHashMap区别总结

HashMap

底层数组+链表+红黑树实现,可以存储null键和null值,线程不安全

初始size为1***6,扩容:newsize = oldsize2,size一定为2的n次幂

扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入

插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)

当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀。

HashMap的初始值还要考虑加载因子:

哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
空间换时间*:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。*

ConcurrentHashMap

底层采用分段的数组+链表实现,线程安全

通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)

Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁

扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

ConcurrentHashMap是使用了锁分段技术来保证线程安全的。

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

TreeMap:

参考文章
参考文章
Map接口派生了一个SortMap子接口,SortMap的实现类为TreeMap。TreeMap也是基于红黑树对所有的key进行排序,有两种排序 方式:自然排序和定制排序。Treemap的key以TreeSet的形式存储,对key的要求与TreeSet对元素的要求基本一致。

基本特点

TreeMap具有如下特点:
1.不允许出现重复的key;
2.可以插入null键(key),null值;
3.可以对元素进行排序;
4.无序集合(插入和遍历顺序不一致);
5.非线程安全的。

LinkedHashMap:

参考
参考
它的父类是HashMap,使用双向链表来维护键值对的次序,迭代顺序与键值对的插入顺序保持一致。LinkedHashMap需要维护元素的插入顺序,so性能略低于HashMap,但在迭代访问元素时有很好的性能,因为它是以链表来维护内部顺序。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

在这里插入图片描述

特点

1.LinkedHashMap继承了HashMap ,实现了Clonable ,serialiable(可序列化) , map接口;
2.提供了AccessOrder参数,用来指定LinkedHashMap的排序方式,accessOrder =false -> 插入顺序进行排序 , accessOrder = true -> 访问顺序进行排序。
3.线程不安全的,

LinkedHashMap的优势:

相比于HashMap:迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。而LinkedHashMap,它虽然增加了时间和空间上的开销,通过一个双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序有两种,可以是插入顺序或者是访问顺序。LinkedHashMap继承了HashMap类,有着HashMap的所有功能,还提供了记住元素添加顺序的方法。

LinkedHashMap的优势:

相比于HashMap:迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。而LinkedHashMap,它虽然增加了时间和空间上的开销,通过一个双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序有两种,可以是插入顺序或者是访问顺序。LinkedHashMap继承了HashMap类,有着HashMap的所有功能,还提供了记住元素添加顺序的方法。

LinkedHashMap和HashMap的异同点:

不同点
1.linkedHashMap虽然继承HashMap, 但实现了双线链表 ,有固定的顺序,与插入entry的顺序一样; 而HashMap存储的方式是无序的。
2.LinkedHashMap包含removeEldestEntry()方法,而HashMap则没有;
相同点
key——value键值对:
1.Key和Value都允许空
2.Key重复会覆盖、Value允许重复
3.安全性:非线程安全
4.继承关系:都实现了Clonable ,serialiable(可序列化),map接口;

感谢各位祝我成长的前辈们:
1:
2
3
4
5
6
7
8

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值