今天开始,研读下jdk的常用类的一些源码,下面是jdk中HashMap的研究。诚然,网上已经很多这方面的总结了,但是,个人只是想单纯地把自己的理解过程进行记录,大牛们就绕路吧,当然,欢迎扔砖头。下面是大体的内容如下:

一、哈希的概述

  1、哈希的概念

  2、哈希要解决的问题

二、java中哈希的实现过程

  1、java中实现哈希的关键步骤

  2、关于resize过程的分析

三、java中HashMap在并发时存在链表死循环问题。

一、哈希的概述

  1、数组和哈希

  在说哈希这个数据结构前,先用数组这个耳闻能详的数据结构进行引出:我们知道,访问数组的常用方法是通过数组游标进行访问,我只要持有数组某个元素对应的游标,我就能在O(1)时间内获取该元素。数组的这个性质让它在查询的时候速度非常快,而究其原因,其实是因为数组中元素位置和游标时一一对应的,确定游标即确定元素。这里的游标,就是哈希结构中的key,元素就是value。

  2、哈希是什么

  上面说到了哈希的key以及value,那到底他们是什么?可以这样理解哈希结构:key就是数组的游标,value就是我们要存储的元素,哈希结构可以理解为数组结构的一种扩展,而数组则是特殊的哈希结构,在哈希结构中,我们通过key可以在O(1)时间内定位value。而哈希结构与数组的区别在于:哈希结构中,key不仅仅是数字,它更可以是对象;哈希结构中元素个数不像数组一样不变,而是可以动态变化的。

  3、如何实现哈希

  所以,其实实现哈希主要是要解决这些问题:首先,我要把对象进行值映射,这样解决了key可以是不同对象的问题;然后,把映射值进行压缩,这样保证对象映射的值集中在某个有限的区间内,内存上才能有效利用这些值进行映射表的建立;最后,我们需要在适当的时候扩展映射表的结构,这样可以解决元素个数动态的问题。  

  然后,下面我们就研读下java中的HashMap是如何解决上面提到的这些问题的

二、java 中的HashMap实现原理

  1、映射的解决方法

  在java中,最顶层的类Object 有一个方法:hashCode,下面是api截图的说明:

  hashCode方法,就是把普通java 对象映射为一个哈希吗的方法,其实在算法底层来说,哈希码是通过对象以及内存地址、对象创建当前时间等等数值来进行运算得出的一个数字,基本上,某段时间内的某个对象,哈希码是独一无二的。

  当然,上面的API说到,一个对象,如果通过equals 比较的结果是相等的话,应该保证hashCode也是相等的,这就要求我们如果重写equals 方法必须重写hashCode方法。java API中这样要求的原因其实是和哈希结构在插入数据时如何确定要插入的key是否存在有关,具体后面说到map的put方法时再展开。

  2、hashCode的压缩问题

  显然,hashCode的范围是很大的,不同对象组对应的hashCode对应的范围可能会很大,例如对象一的hashCode是3,然后对象2的hashCode是30000,如果我们直接用一个30000 + 1(假设游标从0开始)大的数组来映射对应的元素(hashCode为3的放到数组对应游标为3的地方,为30000的放到游标对应为30000位置处),这样,显然中间很多位置是没有元素的,这样那部分空间就拜拜浪费了。所以,我们需要一种算法,把各个对象的hashCode压缩在一个一定的范围内,然后,把hashCode压缩之后的数字作为数组的游标,对应着原来的对象,这样就解决了空间浪费的问题,同时又以数组来实现了哈希的结构。

  在java 中,其实它压缩算法是下面这段代码:

static int indexFor(int h, int length) {
return h & (length-1);
}

  至于为什么h & (length-1) 这个算法可以有效保证把元素压缩在一个范围内而且可以很大程度地避免发生哈希碰撞,更具体的原因,可以参开这篇博客:http://blog.csdn.net/qq_27093465/article/details/52207152,这里不累赘展开。

  3、哈希冲突问题

  上面提到了一个概念叫做哈希冲突问题:当我们在进行数据压缩的时候,理想的哈希压缩算法当然是,n个元素的hashCode刚好都压缩在0-n-1的范围,这样就能最大程度地保证数组中的空间都被用到了,然而,对象之间的hashCode是毫无规律的,这种压缩算法是不存在的。就是说,所有不同哈希码并不能保证压缩之后的映射压缩值都不相等,并没有找到一种算法可以吧值都压缩在一个恰当的范围而且恰好都不相等,压缩数据的范围越小,冲突的几率越大,反之,几率越小,这种由于压缩hashCode之后造成的对应值相等的情况就是哈希冲突问题。

  哈希冲突问题解决方案挺多,但是最常用的一种叫做拉链法,也是java 中的哈希结构所用的方法,它是一种链表数组的结构,看下面示意图:

  上面就是利用一个数组长度为5的链表数组来表示hashCode压缩之后值分别为15,36,41,24这个四个值。显然,这种结构中,查找元素速度肯定是没有数组元素中利用游标进行元素查找那么快的,但是,只要对应的压缩算法能够较好地分散元素,哈希冲突不严重时,查找某个元素还是可以保持为O(1)的复杂度的,同时,也能保证空间较大的利用率。

三、java中put方法的具体执行过程

  有了上面这么多的铺垫,下面再对java的哈希结构中添加元素的过程进行简单的总结。在java的HashMap中,执行添加算法大体要经历以下步骤:首先进行对象与哈希值的映射,然后进行哈希码的压缩操作,再根据压缩得到的结果,找到链表数组的对应链表,看该位置是否存在了元素(不为null),如果该游标元素还没有聊表元素,直接插入;否则,遍历该结果值对应的游标的链表元素,进行链表的遍历,利用eauql方法判断元素是否存在,如果存在,则覆盖旧值;否则,直接添加。 当然,在添加新元素前,需要进行元素个数的判断,当元素个数超过负载因子*数组长度时,就要进行resize操作了。

  下面,对这个过程的更多细节的地方,我们进行一一展开。

  1、put方法的源码分析。先看看put这个方法是如何执行的:

public V put(K key, V value) {
//如果key是null,直接调用特殊的putForNullKey方法进行元素添加
if (key == null)
return putForNullKey(value);
//利用hash函数,计算哈希码
int hash = hash(key);
//进行哈希码的压缩映射
int i = indexFor(hash, table.length);
//遍历压缩之后的映射值对应的游标的链表元素,查看是否包含了新添加的key值。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果存在 了key,直接替换旧value,并返回旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//否则,进行add元素的操作
addEntry(hash, key, value, i);
return null;
}

  put操作的主要操作步骤在注释里面也说了,下面就详细针对put这个方法的addEntry方法进行详细的总结。

  2、addEntry的过程。先看源码和对应的注释:

void addEntry(int hash, K key, V value, int bucketIndex) {
//首先,判断当前map中元素的个数是否超过了threshold这个临界值,这个临界值的计算方式是:负载因子*链表数组长度
//如果超过了,则进行resize(扩容)操作,否则,直接执行createEntry操作(就是直接在链表末端插入新链表节点即可)
if ((size >= threshold) && (null != table[bucketIndex])) {
//执行扩容操作
resize(2 * table.length);
//扩容操作执行之后,为什么要重新计算hash值?这个希望有大牛懂的话可以告知下
hash = (null != key) ? hash(key) : 0;
//扩容之后,length发生了变化,所以bucketIndex需要重新计算
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
}

  这里的addEntry操作,主要就是先判断当前元素个数是否超过了一个临界值,这个临界值是链表数组*负载因子(默认是0.75),超过了的话就要执行扩容操作。不过本人有点不明白的地方是:为什么resize之后,要重新计算对象的hash值的?按道理,hashCode与扩容与否应该无直接关系吧?

  3、resize方法的执行过程。那么resize又是怎么执行的呢?还是先看代码吧:

void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//先判断当前的数组容量是否超过了最大容量允许值,如果超过了,直接设置threshold位Integer.MAX_VALUE并不执行扩容操作
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建扩容新数组
Entry[] newTable = new Entry[newCapacity];
//下面这段代码其实我也查了好久资料,发现网上并没有相关的资料讲解,
//就是oldAltHashing和useAltHashing两个值到底是干嘛的,有大佬知道非常希望可以告知下
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//transfer把旧元素统一移动到新的链表数组上去
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

  这段代码总体不难看懂,不过本人其实也有一点疑问:就是oldAltHashing 和useAltHashing 的含义是什么?这个希望有大牛懂的留言告知下,本人查了挺久也没得到相关的解释。

  总体来说,resize过程就是创建一个双倍大的新链表数组,并重新计算已有所有元素的压缩哈希值,根据新的结果,重新调整哈希表。这个过程,其实是挺耗费性能的。而具体的transfer方法执行过程,还是看源码吧:

void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历旧的链表数组
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算对应的数组链表值
int i = indexFor(e.hash, newCapacity);
//把该节点插入到新数组的对应节点
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

  上面的transfer 过程,主要就是理解e.next = newTable[i];newTable[i] = e;e=next这三个语句:首先是把旧的元素的下一个节点指向新的链表数组的对应元素(一开始为null),当然,旧元素的next元素需要一个next变量进行存储;当下一个新元素也插入同一个位置时,重复上面这个过程的效果是:在旧数组中在后面的元素,在新扩容后数组对应每个链表中,反而在前面了,相当于反转了。如果还不好理解,看下面的示意图:

  

  

三、关于HashMap中多线程下的安全问题

  在了解了HashMap的机制之后,顺便提一个问题:HashMap是非线程安全的,这不仅仅是因为它无法保证数据的一致性,更会有可能形成聊表死循环,造成性能的极度浪费。出现这个问题的根本原因其实是:在transfer 中,执行了旧的链表节点指向新的数组元素时,其他线程刚好执行到next = e.next时,此时,这个线程的next就会存储了新的数组中对应的链表而不是旧的数组的原有链表了,当前线程继续往下执行时,便会出现  节点的next指向自身的情况,参考下面的图进行理解:

  

  所以,HashMap是不能在多线程下使用的,它 不仅会发生数据不一致的问题,而且会造成死循环!

jdk源码研究1-HashMap的更多相关文章

  1. JDK源码分析之hashmap就这么简单理解

    一.HashMap概述 HashMap是基于哈希表的Map接口实现,此实现提供所有可选的映射操作,并允许使用null值和null键.HashMap与HashTable的作用大致相同,但是它不是线程安全 ...

  2. JDK源码分析(三)——HashMap 下(基于JDK8)

    目录 概述 内部字段及构造方法 哈希值与索引计算 存储元素 扩容 删除元素 查找元素 总结 概述   在上文我们基于JDK7分析了HashMap的实现源码,介绍了HashMap的加载因子loadFac ...

  3. JDK源码分析(三)——HashMap 上(基于JDK7)

    目录 HashMap概述 内部字段及构造方法 存储元素 扩容 取出元素 删除元素 判断 总结 HashMap概述   前面我们分析了基于数组实现的ArrayList和基于双向链表实现的LinkedLi ...

  4. 【jdk源码学习】HashMap

    package com.emsn.crazyjdk.java.util; /** * “人”类,重写了equals和hashcode方法...,以id来区分不同的人,你懂的... * * @autho ...

  5. JDK源码学习笔记——HashMap

    Java集合的学习先理清数据结构: 一.属性 //哈希桶,存放链表. 长度是2的N次方,或者初始化时为0. transient Node<K,V>[] table; //最大容量 2的30 ...

  6. jdk源码阅读笔记-HashMap

    文章出处:[noblogs-it技术博客网站]的博客:jdk1.8源码分析 在Java语言中使用的最多的数据结构大概右两种,第一种是数组,比如Array,ArrayList,第二种链表,比如Array ...

  7. 【jdk源码3】HashMap源码学习

    可以毫不夸张的说,HashMap是容器类中用的最频繁的一个,而Java也对它进行优化,在jdk1.7及以前,当将相同Hash值的对象以key的身份放到HashMap中,HashMap的性能将由O(1) ...

  8. jdk源码阅读笔记-HashSet

    通过阅读源码发现,HashSet底层的实现源码其实就是调用HashMap的方法实现的,所以如果你阅读过HashMap或对HashMap比较熟悉的话,那么阅读HashSet就很轻松,也很容易理解了.我之 ...

  9. JDK源码学习笔记——LinkedHashMap

    HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序. LinkedHashMap保证了元素迭代的顺序.该迭代顺序可以是插入顺序或者是访问顺序.通过维护一个 ...

随机推荐

  1. mybatis深入理解之 # 与 $ 区别以及 sql 预编译

    mybatis 中使用 sqlMap 进行 sql 查询时,经常需要动态传递参数,例如我们需要根据用户的姓名来筛选用户时,sql 如下: select * from user where name = ...

  2. JavaScript数组方法大全

    1.两个数组拼接的方法: Array.concat(obj); var array = [1,2,3]; var array2 = [4,5,6]; var arrtotall = array.con ...

  3. Ubuntu 14.04.4 下 scp 远程拷贝提示:Permission denied, please try again. 的解决办法

    我在 s0 主机上远程拷贝 /etc/hosts 文件到 s1 主机上,出现下面的错误提示: qiao@s0:~$ scp /etc/hosts root@s2:/etc/root@s2's pass ...

  4. CSS格式与布局中三种位置的理解与应用

    第一种位置关系:position:fixed  锁定位置(相对于整个浏览器的位置),常用在各大网站的右下角或其它位置的小广告. 如果需要调整锁定位置,需要使用如下方式:<div style=&q ...

  5. @font-face 使用过程

    格式:   TureTpe(.ttf)格式: .ttf字体是Windows和Mac的最常见的字体,是一种RAW格式,因此他不为网站优化,支持这种字体的浏览器有[IE9+,Firefox3.5+,Chr ...

  6. ExtJs 思维导图

    搭建环境: 1. 建tomcat , eclipse2. 复制 Extjs 到 项目的 webcontent下 (建议用自己一个熟悉的Extjs版本,我用Ext JS 4.2), 备份一个ext目录, ...

  7. 最大流&流分布&不确定图网络可靠性

    最大流:确定图上每条边含有可以传递的容量值,找到s-t之间可以传送最大的流值. 流分布:最大流是个值,假设极为f,我们要从s到t传递f的流量,对应了多个方案,每个方案就是一个流分布. 不确定图:不确定 ...

  8. Windows Graphics Programming Win32 GDI and DirectDraw第六章疑问

    <Windows Graphics Programming Win32 GDI and DirectDraw>6.1节中有这样的描述: The Windows NT/2000 graphi ...

  9. fis3+vue+pdf.js制作预览PDF文件或其他

    人生第一篇博客,的确有点紧张,但有些许兴奋,因为这对于我来说应该是一个好的开始,以此励志在技术的道路上越走越远. 看过了多多少少的技术博客,给自己带来了很多技术上的收获,也因此在想什么时候自己也可以赠 ...

  10. java web轻量级开发面试教程内容精粹:哪些简历得不到面试机会

    看到一本较好的实践性比较强的书,java web轻量级开发面试教程,里面的一些内容很有实践意义. 问题点 很难获得面试机会的原因 学历不符,比如要求是本科以上,但学历是大专 学历是硬指标,所以达不到学 ...