Java集合 -- HashMap底层实现HashMap 的长度为什么是2的幂次方ConcurrentHashMap 和 HashtableConcurrentHashMap线程安全的实现

Posted CodeJiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合 -- HashMap底层实现HashMap 的长度为什么是2的幂次方ConcurrentHashMap 和 HashtableConcurrentHashMap线程安全的实现相关的知识,希望对你有一定的参考价值。

1. HashMap底层实现

Map存储键值对的数据结构是 数组+链表 的结构,结合了数组查询数据快和链表增删数据快的优点;用Entry[]存储键值对,Entry为类类型,类里面有四个属性:hash、K、V、next,分别存储哈希值、键对象、值对象、下一个Entry对象引用。


1.1 JDK1.8之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列HashMap 通过 keyhashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

扰动函数指的就是 HashMaphash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀些实现⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞

拉链法将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀个元素都是⼀个链表。若遇到哈希冲突(两个不相等的 key 产生了相等的 hash值),则将冲突的值加到链表中即可。(即一个链表可以存储多个键值对)

JDK 1.8 HashMaphash ⽅法源码:

^(异或):如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0

对⽐⼀下 JDK1.7 HashMaphash ⽅法源码:

相⽐于 JDK1.8 hash ⽅法 ,JDK 1.7 hash ⽅法的性能会稍差⼀点点,因为毕竟扰动了 4 次。


1.2 JDK1.8之后

相⽐于之前的版本, JDK1.8之后在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红⿊树,以减少搜索时间。

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。


2. HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-21474836482147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是 (n - 1) & hashn 代表数组长度。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。

但是%操作中如果除数是2幂次,则等价于与其除数减一&操作,也就是说 hash % length == hash & (length - 1)的前提是 length2n 次方;

&运算:两个同时为1,结果为1,否则为0。

采用二进制位操作 &相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。


3. ConcurrentHashMap 和 Hashtable

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

底层数据结构:

JDK1.7 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组 + 链表 / 红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要):

① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

两者的对比图:

JDK1.7的ConcurrentHashMap:

JDK1.8 的 ConcurrentHashMap(TreeBin: 红⿊⼆叉树节点 Node: 链表节点):


4. ConcurrentHashMap线程安全的实现


4.1 JDK 1.7 实现

JDK1.7 ConcurrentHashMap:

⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁(可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。(连续调用多次lock方法会把自己阻塞住)),扮演锁的角色。HashEntry 用于存储键值对数据。

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁


4.2 JDK 1.8 实现

JDK1.8 的 ConcurrentHashMap(TreeBin: 红⿊⼆叉树节点 Node: 链表节点):

ConcurrentHashMap取消了Segment分段锁,采⽤CASsynchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红⿊⼆叉树。Java 8在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红⿊树(寻址时间复杂度为O(log(N)))。

synchronized只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要hash不冲突,就不会产⽣并发,效率⼜提升N倍。



以上是关于Java集合 -- HashMap底层实现HashMap 的长度为什么是2的幂次方ConcurrentHashMap 和 HashtableConcurrentHashMap线程安全的实现的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合学习--HashMap

Java集合 - HashMap

Java中的容器(集合)之HashMap源码解析

goland map底层原理

Java集合---HashSet的源码分析

算法---hash算法原理(java中HashMap底层实现原理和源码解析)