HashMap简析
来源:互联网 发布:unity3d选中物体高亮 编辑:程序博客网 时间:2024/06/13 10:57
一次电话面试中,面试官询问,HashMap是线程不安全的,那么并发使用时造成死循环的原因是什么。可惜由于我本来知道它是线程不安全的,从未写过并发读取的代码,因此没有遇到过这个问题,只好回答没遇到过。身为java程序员,没有研究过HashMap源码也确实说不过去,遂在面试结束后仔细阅读了JDK7的HashMap.java源码,简单分析如下文。
存储结构
所有数据存储在Entry的数组中,但是注意,并不是一个元素对应数组中的一个Entry,数组中的一个Entry代表一系列序列(indexFor方法计算)相同的键值对,使用Entry的next属性关联成链表,具体可以看get和put方法如何寻找HashMap是否已经包含了key:transient Entry<K,V>[] table;
示意图如下,方框为Entry数组(或者称其为table),数组中每一个元素可能是一个链表:
增加元素
HashMap中put方法先寻找是否已经包含了该key,如果包含,覆盖原来对应的value,并返回旧的value,,否则创建一个新的Entry。
public V put(K key, V value) {// key的index(hash与table.length-1进行与运算)值相同的元素,用Entry链表保存,因此这里要进行循环next,直到找到该key for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value;// 将e的value进行覆盖,并返回oldValue e.value = value; e.recordAccess(this); return oldValue; } } modCount++;// 需要put进来的key在HashMap中不存在,则创建一个新的Entry addEntry(hash, key, value, i); return null; }void addEntry(int hash, K key, V value, int bucketIndex) { // addEntry首先判断是否超出threshold,如果超出,则进行resize,否则直接createEntry if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex];// table[bucketIndex]用新的Entry对象覆盖,Entry的构造方法中将原table[bucketIndex]对象赋值给新Entry的next,形成链表。也就是一直在链表头部新增元素。 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
线程安全
众所周知,HashMap不是线程安全的,那么并发时会出现什么问题呢。首先各种计数器会计算错误,并且同时操作table[i]会造成数据丢失或覆盖。除此以外,由于存储数据采用链表,且put和get中都会对链表进行循环,那么会不会产生闭环链表,导致死循环出现呢。
上面addEntry方法中在数组容量不足时,会进行调用resize方法扩容,该方法重新创建了新的Entry数组,即table变量,并且将老的table中对象使用transfer方法转移到新的table中。
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);// 这里将老的table的数据,转移到新Table中,每个table[i]的链表都进行循环,将newTable[i]赋值给当前Entry对象的next,并将newTable[i]指向当前对象,即一直在链表头部增加原table链表中的next,转移完成后,链表相当于倒转了。 e.next = newTable[i]; newTable[i] = e; e = next; } } }
这个方法很有意思,例如在table[i]中存储了1-->2-->3-->4这样一个链表,transfer方法相当于翻跟头一样,将数据复制到newTable中: 每次循环后newTable[i]的链表变化为:step1:1 step2 :2-->1 step3:3-->2-->1 step4: 4-->3-->2-->1,想象下如果transfer方法同时有两个线程进入,一前一后操作同一个table[i],可能出现这样的情况:
线程一在step3中将newTable[i]赋值为3-->2-->1,而线程二刚走到step2,将newTable[i]更新为2-->3-->2-->1,闭环的链表出现了,后面无论是get还是put操作,在newTable[i]的链表上将产生死循环。
- HashMap简析
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- HashMap
- hashmap
- HashMap
- HashMap
- HashMap
- hashmap
- HashMap
- 基数排序算法的C++实现
- 面试之常用算法总结
- 剑指offer--二进制中1的个数
- 证明素数有无限多个(《具体数学》上的方法)
- 启动动画问题startAnimation
- HashMap简析
- zookeeper 权限概述
- C++刷题二
- ngnix 配置域名和二级域名
- 边学边笔记-Java中的过滤器详细笔记之全局编码统一+html标记转义+脏话过滤
- VC Check box (转载新浪博客于超峰的)
- web前端性能优化小结
- 机器学习入门:线性回归及梯度下降
- 每日一题21:从0打印到具有n位整数的最大数