Java并发编程系列
- Java经典同步锁:Synchronized关键字
- Java面试宝典:final语义深度分析
- 经典hash算法:HashMap深度解析
前言
在JDK1.6中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在同一链表。但是当某一个链表的元素较多时,通过key单链查找效率很低。
而在JDK1.8中,HashMap采用数组+链表+红黑树来实现,当链表长度超过阈值(TREEIFY_THRESHOLD=8)时,将链表转换为红黑树,这样节省查找时间。
红黑树链表阈值
本文采用JDK1.8说明。
概念
散列#Hash
散列就是把任意长度的输入,通过散列算法变成固定长度的输出.
Hash冲突
由于散列是不定长到定长,是一种压缩映射,输出值域小于输入值域,造成不同的输入值可能会有相同的输出值,这就是Hash冲突。
原理分析
hash存储位置
当程序调用put方法时,首先计算key的hash值,然后根据位运算算出是新Node节点还是红黑树(TreeNode)节点。
if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);
如果是红黑树,还需要判断key的equals方法,配合key的hash值,双管齐下,才能确定该节点的红黑树的位置
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
为什么红黑树要通过key的hash和equals方法来确定节点存储位置呢?
这是由于hash算法本身特性造成的。从Hash冲突的概念我们知道,通过Key算hash的存储位置,可能存在存储位置相同!
那如何解决key的hash冲突,保证唯一性呢?
Hash冲突
我们先了解下Java对equals方法和hashCode方法的两个约定:
当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false
也就是说,如果hashCode不同,equals一定为false,如果hashCode相同,equals不一定为true。
理论上,hashCode可能存在冲突的情况,当冲突发生时,计算出的桶索引(bucketIndex)也是相同的,这时会取到桶索引位置已存储的元素,最终通过equals来比较,equals方法就是哈希码碰撞时才会执行的方法。
基于以上两个约定,HashMap通过hashCode和equals判断Key是否已存在,如果hashCode和equals相等,则使用新V值替换旧V值;如果hashCode相等当equals为false,则在JDK1.8中会使用红黑树法来解决冲突并返回旧V值,如果不存在 ,则存放新的键值对到桶索引位置。
一般情况下hash冲突在多线程高并发情况下比较明显。
我们知道JDK1.6是用equals比较来保证key的唯一性,但是某个Node节点(单链表结构)随着内部元素的增多,put和get的效率将越来越低,这里的时间复杂度是O(n),假如有1000个元素,put时需要比较1000次,效率很低。
通常情况下HashMap很少会用到equals方法,因为HashMap通过一个哈希表管理所有元素,当我们调用put存值时,HashMap首先会调用key的hashCode方法,获取哈希码,通过哈希码快速找到某个存放位置。
总结
在多线程情况下,一般不会用HashMap,因为线程不安全,容易才产生脏数据。可以考虑用继承+锁或者ConcurrentHashMap代替。
关于我
我是Wooola,擅长微服务,分布式,并发,工作流。
有需要Java资料的同学,可以关注之后私信哈,回复“资料”可以免费领取Java BAT面试宝典/微服务/SpringCloud/SpringBoot等视频和资料,亲记得要点赞转发哦!!!