今天开始,研读下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. Java微信公众平台开发之公众号支付(微信内H5调起支付)

    官方文档点击查看准备工作:已通过微信认证的公众号,必须通过ICP备案域名(否则会报支付失败)借鉴了很多大神的文章,在此先谢过了 整个支付流程,看懂就很好写了 一.设置支付目录 在微信公众平台设置您的公 ...

  2. JavaScript闭包只学这篇就够了

    闭包不是魔法 这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义. 其实只要理解了核心概念,闭包并不是那么的难于理解.但是,网上充斥了太多学术性的文章 ...

  3. 【机器学习笔记之四】Adaboost 算法

    本文结构: 什么是集成学习? 为什么集成的效果就会好于单个学习器? 如何生成个体学习器? 什么是 Boosting? Adaboost 算法? 什么是集成学习 集成学习就是将多个弱的学习器结合起来组成 ...

  4. 【NO.11】Jmeter - 构建1个可供Linux使用的Jmeter测试脚本 - 共3个步骤

    在Linux使用Jmeter做性能测试需要4个前提条件,这4个前提条件已经在之前的文档里提到了,重复一下加深印象: (1) 在本地已安装xshell 参考<SecureCRT-转换密钥-Xshe ...

  5. 【机器学习笔记之五】用ARIMA模型做需求预测用ARIMA模型做需求预测

    本文结构: 时间序列分析? 什么是ARIMA? ARIMA数学模型? input,output 是什么? 怎么用?-代码实例 常见问题? 时间序列分析? 时间序列,就是按时间顺序排列的,随时间变化的数 ...

  6. linux守护进程、SIGHUP与nohup详解

    前端时间帮忙定位个问题.docker容器故障恢复后,其中的keepalived进程始终无法启动,也看不到Keepalived的日志. strace 查看系统调用之后,发现了原因所在 socket(PF ...

  7. 关于JS中涉及的常用类型转换及运算符表达式

    JS中的常用类型转换(一般用强制转换):1.强制转为整数:parseInt:写法:x = parseInt(x); 2.强制转换位小为:parseFloat:写法:x = parseFloat(x); ...

  8. 理解javascript中的回调函数(callback)

    以下内容来源于:http://www.jb51.net/article/54641.htm 最近在看 express,满眼看去,到处是以函数作为参数的回调函数的使用.如果这个概念理解不了,nodejs ...

  9. js面试题知识点全解(一原型和原型链1)

    1.如何准确判断一个变量是数组类型2.写一个原型链继承的例子3.描述new一个对象的过程4.zepto(或其他框架)源码中如何使用原型链知识点:1.构造函数2.构造函数-扩展3.原型规则和示例4.原型 ...

  10. Ngnix+tomcat负载均衡

    系统环境 Centos6.5 nginx1.4.7  https://nginx.org/download/nginx-1.4.7.tar.gz tomcat7.0.79 http://apache. ...