您好,欢迎来到花图问答。
搜索
您的当前位置:首页HashMap扩展/ConcurrentHashMap/Link

HashMap扩展/ConcurrentHashMap/Link

来源:花图问答

HASHMAP扩展

  • 看了小灰的关于HashMap的漫画,就想要再整理一下关于HashMap的一些知识。
  • 2017.11.29,我们的项目里用到了LRUCache,正好LeetCode也有这题,我就去做了下,先后思考了很多种数据结构,最后我想到了能不能用LinkedHashMap这样有序的Map。所以请了解了下LinkedHashMap。

0x00 散列方法和解决HASH冲突的方法

之前考研复习数据结构的时候背过各种名词,也画过、算过各种解决hash冲突的图和公式,但是当时其实并不理解HashMap,毕竟当时根本没什么开发经验。所以现在有点混淆了,就整理下各种名词:

散列的方法

散列的方法,也就是把key的hashcode分配到table的各个index的slot上的方法有:

(1)直接定址法:比如在一个0~100岁的年龄统计表,我们就可以把年龄作为地址。
(2)平方取中法
(3)除留余数法
取关键字被某个不大于哈希表表长m的数p除后所得余数为哈希地址。该方法的关键是选取p。选取的p应使得散列函数值尽可能与关键字的各位相关。p最好为素数。
(4)随机数法
(5)取模法

Java的HashMap里用的就是我们众所周知的&方法,类似(5),但效率更高:

index = HashCode(Key) & (Length - 1)

解决冲突的:

就是由于key的hashCode相同或者hashCode散列出来的index相同,需要放在一个bucket里的方法,也就是怎么在桶里排序。

(1) 拉链法
这个印象最深了,拉链法就是Java的HashMap里用到的方法,也就是「头插法」,后加上的元素,放在首位,因为设计者认为后加上的元素被访问的概率更大,所以这样遍历链表效率高。我原本以为这样put是一个O(1)操作,但其实也不是,因为你首先要确定这个bucket里是否有个key的hash,如果有,就要覆盖掉这个hash对应的value。我常常会理解错,认为bucket里存放的是value,这是不对的,bucket里存放的是key的hash,value只是hash的附属品,看下下面的图就明白了:


image

(2)开放定址法
不知道这个名字代表的啥了,我印象里反正就是把hash先定位到一个slot(那显然就不是链表,而是数组了),然后往左或者往右挪,直到找到一个slot坑位。

0x01 PUT要点

简单说下PUT过程:

对key的hashCode()做hash,然后再计算index;
如果没碰撞直接放到bucket里;
如果碰撞了,以链表的形式存在buckets后;
如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
如果节点已经存在就替换old value(保证key的唯一性)
如果bucket满了(超过load factor*current capacity),就要resize。

注意几个点吧,

  1. hash时用的一般是取模法,发生collision(hash冲突,碰撞)的时候用拉链法之类的解决hash冲突。
  2. hash碰撞导致一个bucket的链表长度大于TREEIFY_THRESHOLD的时候就转换成红黑树,put/get一次的复杂度变为O(logn)
  3. 如果默认的capacity是16,capacity是0.75,那么当12个桶里都有元素的时候,就会自动resize,这样你在一个桶里找东西变得容易了呀。
  1. get的时候,后插入的节点会在Entry的前面(头插法)。因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。注意,因为Entry是链表,所以是有序的,这个跟bucket的排列不同。

  2. HashMap初始长度是16(1111B)。长度之所以是2的幂是因为,HashMap的Hash并不是用mod(length -1)完成的,而是用 & (length - 1)完成的,众所周知&效率比%高。而用&的话,当然低位都是1最好了,这样Hash Collision的几率最小。

0x02 高并发下的HashMap产生的循环链表

  1. Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
    HashMap.Size >= Capacity * LoadFactor。

  2. Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

这样Resize之后get一个不存在的key的时候就会陷入死循环:


image.png

0x03 ConcurrentHashMap解决HashMap的线程安全问题

HashTable或者Collections.synchronizedMap都是线程安全的(HashTable与HashMap的另一个区别是HashTable不允许空的key-value)。但是他们都是对整个集合加锁,导致同时间的其他操作阻塞(不能同事put和get了,我们知道HashMap之所以不安全,是因为put的时候rehash的时候产生的循环链表)。

ConCurrentHashMap用了Segment,每个Segment读写自治,Segment间不影响。我们知道HashMap线程不安全是产生在并发写入的时候,那
Case1:不同Segment的并发写入
Case2:同一Segment的一写一读
这两个Case本来就是没问题的;只有在
同一Segment并发put才会上锁:


image.png

concurrenthashmap中大量运用了volatile。

另外要注意,小灰的几个漫画都是基于Java1.7,在1.8中,上面提到,hash碰撞导致一个bucket的链表长度大于TREEIFY_THRESHOLD的时候就转换成红黑树,put/get一次的复杂度变为O(logn)。

0x04 LinkedHashMap与LruCache

首先,LinkedHashMap为什么能实现顺序存储Entry,我们知道HashMap是用了& 2的幂的Hash算法把key的hash hash到table(数组)上去,所以它的bucket(Entry)的顺序是乱的,LinkedHashMap总不能为了保持顺序就不去hash吧,那样就不是HashMap了,Hash本身就是为了解决冲突->快速访问存在的,如果顺序存储,那就成了ArrayList之类的结构了。
想到这儿,我又开了脑洞,为什么HashMap做不到像ArrayList那样一个bucket只存一个数据?
为什么ArrayList的查找只能是遍历,不能像Map一样是O(1)?
每当有这个疑问的时候,就看下这个Hash公式吧:

index = HashCode(Key) & (Length - 1)

  1. 之所以可以准确定位,是因为bucket存放的,是key的hash经过与操作后得到的index,存是这么存,取也根据这个取,所以是O(1);
  2. 之所以不能一个bucket存放一个数据,是因为Hash算法总会发生冲突,有两种可能
    (1) 不同的key,hashCode相同
    (2)不同的hashCode,与操作得到的index相同,比如默认Capacity16,1011010101110 1001 & 1111得到index = 1001B = 9,只是判断最后4bit的,自然会有冲突。

花图问答还为您提供以下相关内容希望对您有帮助:

hashmap和concurrenthashmap的区别,hashmap的底层源码

HashMap:非线程安全。在多线程环境下,如果多个线程同时访问和修改HashMap,可能会导致数据不一致的问题。ConcurrentHashMap:线程安全。它通过内部机制确保在多线程环境下数据的一致性和高效性。性能:HashMap:在单线程环境下性能较高,因为它没有额外的同步开销。ConcurrentHashMap:虽然线程安全,但在多线...

HashMap的底层实现以及ConcurrentHashMap的底层实现

综上所述,HashMap和ConcurrentHashMap的底层实现均随着JDK版本的更新而不断优化和改进,以适应更高的并发需求和性能要求。

HashMap结构组成及扩容原理

HashMap的结构组成主要包括数组、链表和红黑树,其扩容原理涉及resize和rehash两个步骤。结构组成: 数组:HashMap 的基础存储结构是一个数组,数组的每个元素被称为一个桶。 链表:当多个元素通过哈希计算得到的下标相同时,这些元素会被存储在同一个桶中的链表中。 红黑树:当链表长度超过一定阈值时,为...

HashMap HashTable和ConcurrentHashMap的区别

Hashtable:所有方法都是同步的,因此是线程安全的。但在单线程环境中,同步会带来不必要的性能开销。HashMap:方法不是同步的,因此在多线程环境中使用时需要外部同步。ConcurrentHashMap:内部采用了分段锁,允许高并发访问而无需外部同步,且性能优于Hashtable。对null值的支持:Hashtable:不允许任何null...

ConcurrentMap和HashMap的区别

HashMap:允许使用null作为键或值。ConcurrentHashMap:不允许使用null作为键或值。如果尝试插入null键或值,将抛出NullPointerException。并发性能:HashMap:在单线程环境下性能较好,但在多线程环境下由于需要额外的同步处理,性能可能会大幅下降。ConcurrentHashMap:专为并发环境设计,通过分段锁等技术实现了...

hashmap和concurrenthashmap的区别是什么?

HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在。那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行...

ConcurrentHashMap是如何保证线程安全的?

ConcurrentHashMap相当于是HashMap的多线程版本,它的功能本质上和HashMap没什么区别。因为HashMap在并发操作的时候会出现各种问题,比如死循环问题、数据覆盖等问题。而这些问题,只要使用ConcurrentHashMap就可以完美地解决。那问题来到了,ConcurrentHashMap它是如何保证线程安全的呢?1、JDK1.7实现原理首先,...

HashMap、HashTable、HashSet、concurrentHashMap 线程安全,区别,实现...

HashTable:线程安全。它在每次更改时都会进行同步,因此效率相对较低。 HashSet:非线程安全。因为它是基于HashMap实现的,所以同样存在线程安全问题。如果需要在多线程环境下使用,需要进行额外的同步处理。 concurrentHashMap:线程安全。它是Java 5引入的专门用于高并发场景的Map实现。区别: HashMap:允许...

hashmap和concurrenthashmap的区别是什么?

最大的区别就是ConcurrentHashMap是线程安全的,hashMap不是线程安全的。基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。此...

HashMap、HashTable、ConcurrentHashMap的原理与区别

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,...

Copyright © 2019- huatuowenda.com 版权所有 湘ICP备2023022495号-1

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务