HashMap底层原理

HashMap底层原理

在说HashMap底层原理之前,我们先来说一下HashMap与Hashtable和ConcurrentHashMap的区别。这篇文章主要去说JDK1.8版本的HashMap,其实他们的原理都差不多,都是数组加链表的存储形式。再讲之前我们先了解一下Map的继承体系:

在这里插入图片描述

HashMap与Hashtable的区别

相同点

  • HashMap和Hashtable都是基于哈希表实现的,其内部每个元素都是key-value 键值对,
  • HashMap和Hashtable都实现了Map、Cloneable、 Serializable 接口。

不同点

  • 父类不同: HashMap继承了AbstractMap 类,而Hashtable继承了Dictionary
    在这里插入图片描述

  • 空值不同: HashMap 允许空的key和value值,HashTable 不允许空的key和value值。
    HashMap会把Null key当做普通的key对待。不允许null key重复。
    在这里插入图片描述

  • 线程安全性: HashMap 不是线程安全的,如果多个外部操作同时修改HashMap的数据结构比如add或者是delete,必须进行同步操作,仅仅对key或者value的修改不是改变数据结构的操作。可以选择构造线程安全的Map比如Collections.synchronizedMap或者是ConcurrentHashMap。而Hashtable本身就是线程安全的容器。

  • 性能方面:虽然HashMap和Hashtable都是基于单链表的,但是HashMap进行put或者get操作,可以达到常数时间的性能;而Hashtable的put和get操作都是加synchronized 锁的,所以效率很差。
    在这里插入图片描述

  • 初始容量不同: Hashtable 的初始长度是11,之后每次扩充容量变为之前的2n+1 (n为上一次的长度)而HashMap的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么Hashtable 会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

HashMap和ConcurrentHashMap的区别

浅谈ConcurrentHashMap本质

我们为什么要使用ConcurrentHashMap

在并发编程中,jdk1.7的情况下使用 HashMap 可能造成死循环,而jdk1.8 中有可能会造成数据丢失

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

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

jdk1.8中结构
在这里插入图片描述

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

区别总结

HashMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

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个写线程执行,并发性能的提升是显而易见的。

HashMap存取元素详解

HashMap的存储方式是哈希表,那什么是哈希表呢,其实就是数组+链表。HashMap初始数组长度为16。数组的每个元素都保存着链表头的地址(或者为null),在向HashMap中put(key,value)的时候,先使用hash算法计算哈希值,然后再和数组的长度减一做与运算。计算出此键值对应该保存到数组的那个位置上,如果此位置没有元素,意思就是链表的头结点为null,那么就新建一个node结点,把key,value以及next保存。Node类源码如下:

static class Node<K,V> implements Map.Entry<K,V> 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值