HashMap 和 ConcurrentHashMap比较
基础知识:
1. ConcurrentHashMap:
(JDK1.7) segment数组,分段锁;segment 内部是 HashEnty数组,类似HashMap;
统计长度的方法,先不加锁统计两次,如果一样即为长度,否则加锁,重新统计。先采用不加锁的方式,连续计算元素的个数,最多计算3次:如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
两倍的方式扩充。方便扩容时数据移动,数据移动位置一般是当前位置或者 当前位置+原数组长度(增加的长度,根据是0或者1来判断,更高效。)
tryLock获取锁,获取不到调用scanAndLockForPut()尝试,尝试64次后还不行,则挂起。等待线程释放锁,唤醒。
(JDK1.8)放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现。
如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;
如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;
如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值
如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;
1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount
初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化;
如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组
如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象;
统计长度的方法: 在1.8中的size实现比1.7简单多,因为元素个数保存baseCount中,部分元素的变化个数保存在CounterCell数组中
2. HashMap:
(JKD1.7) 数组 + 链表 ;
hash函数,通过hash函数计算出下标;
put和get方法,先进行hash 得到位置,然后有没有值,如果有值,再通过equal方法比较 链表上的所有值;如果没有相同的,新值放在链表的最上面。
HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。
为什么线程不安全?通过Entry内部的next变量可以知道使用的是链表,这时候我们可以知道,如果多个线程,在某一时刻同时操作HashMap并执行put操作,而有超过两个key的hash值相同,如图中a1、a2,
这个时候需要解决碰撞冲突,而解决冲突的办法上面已经说过,对于链表的结构在这里不再赘述,暂且不讨论是从链表头部插入还是从尾部初入,这个时候两个线程如果恰好都取到了对应位置的头结点e1,
而最终的结果可想而知,a1、a2两个数据中势必会有一个会丢失。
当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的
均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。
(JDK1.8) 负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表
长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
链表长度 到8以后,转换为红黑树。长度减小到6以后,重新转换为链表。
transient Node<K,V>[] table; // 1.8 以后的数组
HashMap里面的红黑树是 TreeNode ,
ConcurrentHashMap 红黑树对象是TreeBin,TreeBin里面有 TreeNode<K,V> root; 成员变量,还有一个 lockState ,估计是为了方便加锁。
3. LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
HashMap和双向链表合二为一即是LinkedHashMap。所谓LinkedHashMap,其落脚点在HashMap,因此更准确地说,它是一个将所有Entry节点链入一个双向链表双向链表的HashMap。
在LinkedHashMapMap中,所有put进来的Entry都保存在如下面第一个图所示的哈希表中,但由于它又额外定义了一个以head为头结点的双向链表(如下面第二个图所示),因此对于每次put进来Entry,
除了将其保存到哈希表中对应的位置上之外,还会将其插入到双向链表的尾部。
1. 如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,
并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得Hash碰撞的概率又小,哈希桶数组(Node<k,v>[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制。
当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高。
2. Node<k,v>[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。
也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
3. 结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是
对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率
要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
4. 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以
参考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小为11,就是桶大小设计为素数的应用(Hashtable扩容后不能保证还是素数)。HashMap采用这种非常规设计,主要是为了
在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
5. 这里存在一个问题,即使本文不再对红黑树展开讨论,想了解更多红黑树数据结构的工作原理可以参考http://blog.csdn.net/v_july_v/article/details/6105630。
6. 这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。
7. 对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是,模运算的消耗还是比较大的,在HashMap中是这样做的:它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,
h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
8. 在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,
也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
9. 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,
方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
10. 红黑树和链表之间互相转换的临界值是6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,
链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
11. 经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。
12. 有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。有兴趣的同学可以研究下JDK1.8的resize源码,写的很赞
13. HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,所有的Hash算法结果
得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 鉴于JDK1.8做了多方面的优化,总体性能优于JDK1.7,下面我们从两个方面用例子证明这一点。
小结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
(5) 还没升级JDK1.8的,现在开始升级吧。HashMap的性能提升仅仅是JDK1.8的冰山一角。
参见:https://tech.meituan.com/java-hashmap.html
HashMap 和 ConcurrentHashMap比较的更多相关文章
- [Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.
注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享. 原文地址: http://blog.csdn.net/zhanger ...
- HashMap和ConcurrentHashMap流程图
本文表达HashMap和ConcurrentHashMap中的put()方法的执行流程图,基于JDK1.8的源码执行过程. HashMap的put()方法: ConcurrentHashMap的put ...
- HashMap与ConcurrentHashMap的测试报告
日期:2008-9-10 测试平台: CPU:Intel Pentium(R) 4 CPU 3.06G 内存:4G 操作系统:window server 2003 一.HashMap与Concurre ...
- 轻松理解 Java HashMap 和 ConcurrentHashMap
前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据. 本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 ...
- Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 今天发一篇”水文”,可能很多读者都会表示不理解,不过我想把它作为并发序列文章中不可缺少的一块来介绍.本来以为花不了 ...
- Java中关于Map的使用(HashMap、ConcurrentHashMap)
在日常开发中Map可能是Java集合框架中最常用的一个类了,当我们常规使用HashMap时可能会经常看到以下这种代码: Map<Integer, String> hashMap = new ...
- Java7/8 中 HashMap 和 ConcurrentHashMap的对比和分析
大家可能平时用HashMap比较多,相对于ConcurrentHashMap 来说并不是很熟悉.ConcurrentHashMap 是 JDK 1.5 添加的新集合,用来保证线程安全性,提升 Map ...
- 高并发第九弹:逃不掉的Map --> HashMap,TreeMap,ConcurrentHashMap
平时大家都会经常使用到 Map,面试的时候又经常会遇到问Map的,其中主要就是 ConcurrentHashMap,在说ConcurrentHashMap.我们还是先看一下, 其他两个基础的 Map ...
- 深入理解HashMap和concurrentHashMap
原文链接:https://segmentfault.com/a/1190000015726870 前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据. 本篇 ...
- 沉淀再出发:java中的HashMap、ConcurrentHashMap和Hashtable的认识
沉淀再出发:java中的HashMap.ConcurrentHashMap和Hashtable的认识 一.前言 很多知识在学习或者使用了之后总是会忘记的,但是如果把这些只是背后的原理理解了,并且记忆下 ...
随机推荐
- finecms如何调用多个栏目的子栏目
前面我们说到了finecms如何调用多个指定栏目的内容,finecms如何调用多个栏目的子栏目呢?用下面的代码就可以实现了,其中id是具体的栏目id,用“,”逗号隔开 {list action=cat ...
- 20170728 Celery项目 后台处理SQL SERVER的一个异常
-- 1. 感觉是访问DB 失败了,失败原因不明确 -- 2. 考虑解决问题的解决办法,后台记录异常,并重新发送任务. 具体如何来实现
- 数据库---初识sql语句
初识sql语句 SQL语言主要用于存取数据.查询数据.更新数据和管理关系数据库系统,SQL语言由IBM开发.SQL语言分为3种类型: DDL语句 数据库定义语言: 数据库.表.视图.索引.存储 ...
- 802.11n 连接的建议设置是什么?
这些是用于支持 802.11N 的英特尔无线适配器的默认设置. 这些建议采用的设置可以在英特尔® PROSet/ 无线软件的 高级菜单上找到. 属性 值 频带 2.4 的 802.11n 通道宽度 自 ...
- boost生成json中的put操作
ptree中的put操作后可以加<>,指定类型,不加<>采用默认的类型,感觉不加反而更好用.用法见下面例子. #include <iostream> #includ ...
- Scala集合(二)
将函数映射到集合 map方法 val names = List("Peter" , "Paul", "Mary") names.map(_. ...
- Spark中的partition和block的关系
hdfs中的block是分布式存储的最小单元,类似于盛放文件的盒子,一个文件可能要占多个盒子,但一个盒子里的内容只可能来自同一份文件.假设block设置为128M,你的文件是250M,那么这份文件占3 ...
- Tf中的SGDOptimizer学习【转载】
转自:https://www.tensorflow.org/api_docs/python/tf/train/GradientDescentOptimizer 1.tf.train.GradientD ...
- ERROR [main] zookeeper.RecoverableZooKeeper: ZooKeeper create failed after 4 attempts
ERROR [main] zookeeper.RecoverableZooKeeper: ZooKeeper create failed after 4 attempts ERROR [main] m ...
- jmeter BeanShell断言(一)
原文地址https://blog.csdn.net/lijing742180/article/details/81157947 原文地址https://blog.csdn.net/zailushang ...