Map知识(HashMap、TreeMap、ConcurrentHashMAp、HashTable)

文章单纯记录学习,答案不一定正确,大多从网上查阅,有错误望指正


一、HashMap底层数据结构以及扩容

HashMap的底层实现:JDK1.7是数组+链表;JDK1.8是数组+链表/红黑树

1. 扩容的条件:答:JDK1.7版:当要添加新Entry对象时发现(1)size达到threshold(阈值)(2)table[index]!=null时,两个条件同时满足会扩容

JDK1.8版:当要添加新Entry对象时发现(1)size达到threshold(阈值)(2)当table[index]下的结点个数达到8个但是table.length又没有达到64。两种情况满足其一都会导致数组扩容

而且数组一旦扩容,不管哪个版本,都会导致所有映射关系重新调整存储位置。

扩容:

2. put(key,value) -JDK1.7

(1)当第一次添加映射关系时,数组初始化为一个长度为16的HashMap$Entry的数组,这个HashMap$Entry类型是实现了java.util.Map.Entry接口

(2)特殊考虑:如果key为null,index直接是[0]

(3)在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中

(4)计算index = table.length-1 & hash;

(5)如果table[index]下面,已经有映射关系的key与我要添加的新的映射关系的key相同了,会用新的value替换旧的value。

(6)如果没有相同的,会把新的映射关系添加到链表的头,原来table[index]下面的Entry对象连接到新的映射关系的next中。

(7)添加之前先判断if(size >= threshold  &&  table[index]!=null)如果该条件为true,会扩容

if(size >= threshold  &&  table[index]!=null){

①会扩容

②会重新计算key的hash

③会重新计算index

}

3.put(key,value) -JDK1.8

(1)当第一次添加映射关系时,数组初始化为一个长度为16的HashMap$Node的数组,这个HashMap$Node类型是实现了java.util. Map.Entry接口

(2)在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中

> JDK1.8关于hash(key)方法的实现比JDK1.7要简洁, key.hashCode() ^ key.Code()>>16;(异或其高16位)

(3)计算index = table.length-1 & hash;

(4)如果table[index]下面,已经有映射关系的key与我要添加的新的映射关系的key相同了,会用新的value替换旧的value。

(5)如果没有相同的,

①table[index]链表的长度没有达到8个,会把新的映射关系添加到尾部

②table[index]链表的长度达到8个,但是table.length没有达到64,会先对table进行扩容,然后再添加

③table[index]链表的长度达到8个,并且table.length达到64,会先把该分支进行树化,结点的类型变为TreeNode,然后把链表转为一棵红黑树

④table[index]本来就已经是红黑树了,那么直接连接到树中,可能还会考虑考虑左旋右旋以保证树的平衡问题

(6)添加完成后判断if(size > threshold ){

①会扩容

②会重新计算key的hash

③会重新计算index

}


二、HashMap的各种参数

阈值 = size(数组大小)* loadFactory(加载因子)

初始化大小size为16,加载因子为0.75,JDK1.8链表长度为8,数组长度大于等于64,进行树化

(1)1. HashMap的负载因子是多少,为什么?

a) 0.75;

b) 因为定的太大,比如为1,就意味着数组的空位都需要填满,如果一直等到数组填满才扩容,虽然达到了最大的数组空间利用率,但会产生大量的哈希碰撞,同时产生更多的链表,显然不符合我们的要求;

c) 但如果设置的太小,比如0.3,这样一来保证了数组空间很充足,减少了哈希碰撞,这种情况下查询效率很高,但是消耗浪费了大量的空间。

d) 所以我们需要在时间和空间上做一个折中,选择最合适的负载因子以保证最优化,0.75。

2. HashMap的数组长度是2的指数次幂,为什么。

i. 通过tableSizeFor()方法让最高位1的后面全变为1,在让结果n+1,即得到了2的整数次幂。

ii. 因为添加元素时索引的下标可以通过取模运算获得,但是取模的效率低于乘法,所以要避免频繁的取模运算。有因为在我们HashMap中他要通过取模去定位我们的索引,并且hashmap是在不断的扩容的,数组一旦达到容量阈值时就需要进行扩容,那么扩容就意味着要进行数组元素的移动,每一次移动都要重新计算索引,这个过程中牵扯大量的元素移动,就需要频繁的取模运算,就会大大影响效率。

iii. 那么如果我们直接使用与运算,这个效率就远高与取模运算。利用与运算的就需要规定好数组的长度n,将n-1就会得到最高位为0,后面全1的二进制数,与任何数相与得到的范围在0~n-1。

iv. 结论:首先使用与运算来加快计算的效率,而要使用与运算,就需要数组长度n-1与hash值保证其在数组范围内,只有当数组的长度为2的指数次幂时,其计算出来的值才能和取模算法的值相等,并且保证能取到数组中的每一位,减少哈希碰撞,不浪费大量数组资源。

3. 为什么链表长度大于8时转成红黑树?

涉及泊松分布,以下是通过泊松分布计算出来的链表中元素个数和概率的对照表,链表中元素个数为8的概率非常小。

另外,红黑树平均查找长度是log(n),长度为8时,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,才有转换为树的必要。


三、并发下HashMap安全吗?为什么?

线程不安全,因为在多线程下,transfer方法将原table的值赋值给新的table,而transfer使用的是头插法,也就是链表的顺序会翻转,从而可能形成循环链表。

解决方法 : https://blog.csdn.net/javageektech/article/details/103724851

第一种方法,使用Hashtable线程安全类;

第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;

第三种方法,使用并发包中的ConcurrentHashMap类;

前两种方法都是通过给加全表锁来保证安全,在竞争激烈的多线程环境下性能非常差,所以不推荐使用!

,因为hashMap 是数组 + 链表的数据结构,如果我们把数组进行分割多段,对每一段分别设计一把同步锁,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样是不是可以有效的提高性能?


四、ConcurrentHashMap底层实现及其安全机制

好文:https://blog.csdn.net/javageektech/article/details/103724851

Jdk1.7实现方式ConcurrentHashMap类所采用的正是分段锁的思想,将HashMap 进行切割,把HashMap 中的哈希数组切分成小数组,每个小数组有n HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment

Jdk1.8实现方式:JDK1.8中 ConcurrentHashMap 类取消了Segment 分段锁,采用CAS+synchronized来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!


一、Jdk1.7源码分析:

1、 HashEntry和HashMap中的 Entry非常类似,唯一的区别就是其中的核心数据如value ,以及next都使用了volatile关键字修饰,保证了多线程环境下数据获取时的可见性!

2、 Segment 这个静态内部类继承了ReentrantLock类,ReentrantLock是一个可重入锁。

3、 理论上 ConcurrentHashMap 支持 concurrentLevel(通过 Segment 数组长度计算得来,默认为16) 个线程并发操作,每当一个线程独占一把锁访问 Segment 时,不会影响到其他的 Segment 操作,效率大大提升!

4、 Put方法的流程:

第一步,尝试获取对象锁,如果获取到返回 true,否则执行scanAndLockForPut方法,这个方法也是尝试获取对象锁;

第二步,获取到锁之后,类似 hashMap 的 put 方法,通过 key 计算所在 HashEntry 数组的下标;

第三步,获取到数组下标之后遍历链表内容,通过 key 和 hash 值判断是否 key 已存在,如果已经存在,通过标识符判断是否覆盖,默认覆盖;

第四步,如果不存在,采用头插法插入到 HashEntry 对象中;

第五步,最后操作完整之后,释放对象锁;

上述提到的scanAndLockForPut方法,操作也是分以下几步:

 当前线程尝试去获得锁,查找 key 是否已经存在,如果不存在,就创建一个 HashEntry 对象;

 如果重试次数大于最大次数,就调用lock()方法获取对象锁,如果依然没有获取到,当前线程就阻塞,直到获取之后退出循环;

 在这个过程中,key 可能被别的线程给插入,所以在第 5 步中,如果 HashEntry 存储内容发生变化,重置重试次数;

通过scanAndLockForPut()方法,当前线程就可以在即使获取不到segment锁的情况下,完成需要添加节点的实例化工作,当获取锁后,就可以直接将该节点插入链表即可。

这个方法还实现了类似于自旋锁的功能,循环式的判断对象锁是否能够被成功获取,直到获取到锁才会退出循环,防止执行 put 操作的线程频繁阻塞,这些优化都提升了 put 操作的性能。

5、 remove方法:

1) 先获取对象锁;

2) 计算 key 的 hash 值在 HashEntry[]中的角标;

3) 根据 index 角标获取 HashEntry 对象;

4) 循环遍历 HashEntry 对象,HashEntry 为单向链表结构;

5) 通过 key 和 hash 判断 key 是否存在,如果存在,就移除元素,并将需要移除的元素节点的下一个,向上移;

6) 最后就是释放对象锁,以便其他线程使用;

二、 jdk1.8源码分析

1、 JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见行!

2、 Put方法流程:

 首先会判断 key、value 是否为空,如果为空就抛异常!

 接着会判断容器数组是否为空,如果为空就初始化数组;

 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;

 在接着判断f.hash == -1是否成立,如果成立,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;

 获得该位置下的锁,把新的Node节点按链表或红黑树的方式插入到合适的位置;

 节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;

 最后,插入完成之后,进行扩容判断;

3、 initTable 初始化数组,当第一次插入时,内部容器数组为空,该方法进行初始化

其中sizeCtl 是一个对象属性,使用了 volatile 关键字修饰保证并发的可见性,默认为 0,当第一次执行 put 操作时,通过Unsafe.compareAndSwapInt()方法,俗称CAS,将 修改为 -1,有且只有一个线程能够修改成功,接着执行 table 初始化任务。

如果别的线程发现sizeCtl<0,意味着有另外的线程执行 CAS 操作成功,当前线程通过执行Thread.yield()让出 CPU 时间片等待 table 初始化完成。

4、 helpTransfer 帮组扩容,当f.hash =-1时,说明当前f是一个forwardingNode节点,意味着其他线程正在扩容,则一起进行扩容操作。

这个过程,操作步骤如下:

 第 1 步,对 table、node 节点、node 节点的 nextTable(新table)进行数据校验;

 第 2 步,根据数组的 length 得到一个标识符号;

 第 3 步,进一步校验 nextTab、tab、sizeCtl 值,如果 nextTab 没有被并发修改并且 tab 也没有被并发修改,同时 sizeCtl < 0,说明还在扩容;

 第 4 步,对 sizeCtl 参数值进行分析判断,如果不满足任何一个判断,将sizeCtl + 1, 增加了一个线程帮助其扩容;

5、 addCount扩容判断

这个过程,操作步骤如下:

 第 1 步,利用 CAS 将方法更新 baseCount 的值

 第 2 步,检查是否需要扩容,默认 check = 1,需要检查;

 第 3 步,如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;

 第 4 步,如果不在扩容,将 sizeCtl 更新为负数,并进行扩容处理;

总结:

为解决HashMap在多线程环境下操作不安全的问题,java为我们提供了ConcurrentHashMap类,保证了多线程下hashMap操作安全。

在jdk1.7中,ConcurrentHashMap采用了分段锁策略,将一个HashMap切割成Segment数组,而Segment存储n个HashEntry,类似HashMap,不同的是Segment继承自ReetrantLock(可重入锁),在操作的时候给Segment赋予一个对象锁,从而保证了多线程环境下并发操作安全。

但是jdk1.7中,hashMap容易因为冲突链表长度过长,导致查询效率低。

在jdk1.8中,ConcurrentHashMap采用了jdk1.8中HashMap类似的存储结构(数组+链表/红黑树),但ConcurrentHashMap并没有采用分段锁的策略,而是在元素的节点上采用CAS+synchronized操作来保证并发的安全性。

五、 TreeMap和HashMap的区别

1、 HashMap中元素是无序的;TreeMap中所有元素都是有某一固定顺序的

2、 HashMap继承AbstractMap类,是基于hash表实现的;而TreeMap继承SortedMap类,是基于红黑树实现的。

3、 两者都是线程不安全的

4、 HashMap适用于Map插入、删除、定位元素;TreeMap适用于自然顺序或自定义顺序遍历键。

六、HashMap与Hashtable的区别

1、hashtable是线程安全的,即hashtable的方法都提供了同步机制,底层加了synchronized关键字;hashmap不是线程安全的,即不提供同步机制 

2、hashtable不允许插入空值,hashmap允许!

3、HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口。

4、HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。

5、HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1。

6、HashMap和Hashtable的底层实现都是数组+链表结构实现。

7、两者计算hash的方法不同:

Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,562评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,205评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,786评论 2 7