hashcode是什么意思_面试官:说一下HashMap原理,为什么会产生死循环
Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 JDK1.7 和 1.8 中具体实现稍有不同。
今天我们只讲解JDK1.7版本的HashMap。
1、HashMap的数据结构图
是一个数组+链表结构
2、HashMap成员变量
/** * The default initial capacity - MUST be a power of two. */static final int DEFAULT_INITIAL_CAPACITY = 16;/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */static final int MAXIMUM_CAPACITY = 1 << 30;/** * The load factor used when none specified in constructor. */static final float DEFAULT_LOAD_FACTOR = 0.75f;/** * The table, resized as necessary. Length MUST Always be a power of two. */transient Entry[] table;/** * The number of key-value mappings contained in this map. */transient int size;/** * The next size value at which to resize (capacity * load factor). * @serial */int threshold;/** * The load factor for the hash table. * * @serial */final float loadFactor;
这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思?
① DEFAULT_INITIAL_CAPACITY :初始化桶大小(16),因为底层是数组,所以这是数组默认的大小。
② MAXIMUM_CAPACITY :桶最大值。
③ DEFAULT_LOAD_FACTOR :默认的负载因子(0.75)
④ table:真正存放数据的数组。
⑤ size:map中存放的键值对的数量。
⑥ threshold:resize扩容时的阈值。
⑦ loadFactor:负载因子,可在初始化时显式指定。
HashMap 的构造函数可以指定参数也可以无参:
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init();} public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init();}
默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。
3、Entry类
根据代码可以看到真正存放数据的是:transient Entry[] table,这个数组,那么它又是如何定义的呢?
static class Entry implements Map.Entry { final K key; V value; Entry next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry n) { value = v; next = n; key = k; hash = h; } ......}
Entry 是 HashMap 中的一个内部类,从它的成员变量很容易看出:
① key 就是写入时的键。
② value 自然就是值。
③ 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
④ hash 存放的是当前 key 的 hashcode。
知晓了基本结构,那来看看其中重要的put、get方法。
4、put 方法
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry 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 = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}
① 如果 key 为空,则 put 一个空值进去。
② 根据 key 计算出 hashcode。
③ 根据计算出的 hashcode 定位出所在桶。
④ 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
⑤ 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
void addEntry(int hash, K key, V value, int bucketIndex) { 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 e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++;}
① 当调用 addEntry 写入 Entry 时需要判断是否需要扩容。
② 如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。
③ 而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在该位置形成链表。新new的Entry会加到链表的头部。
5、get 方法
public V get(Object key) { if (key == null) return getForNullKey(); Entry entry = getEntry(key); return null == entry ? null : entry.getValue();}final Entry getEntry(Object key) { int hash = (key == null) ? 0 : hash(key); for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null;}
① 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。
② 判断该位置是否为链表。
③ 不是链表就根据 key、key 的 hashcode 是否相等来返回值。
④ 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。
⑤ 啥都没取到就直接返回 null 。
6、并发场景下出现死循环
多线程同时put时,如果同时调用了resize操作,可能会导致循环链表产生,进而使得后面get的时候,会死循环。下面详细阐述循环链表如何形成的。
resize函数
数组扩容函数,主要的功能就是创建扩容后的新数组,并且将调用transfer函数将旧数组中的元素迁移到新的数组。
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; }//创建一个新的Hash Table Entry[] newTable = new Entry[newCapacity]; //将Old Hash Table上的数据迁移到New Hash Table上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor);}
transfer函数
transfer逻辑其实也简单,遍历旧数组,将旧数组元素通过头插法的方式,迁移到新数组的对应位置问题出就出在头插法。
void transfer(Entry[] newTable) { //src旧数组 Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length; j++) { Entry e = src[j]; if (e != null) { src[j] = null; do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null);//由于是链表,所以是个循环过程 } }}static int indexFor(int h, int length) { return h & (length-1);}
下面举个实际例子:
① 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
② 最上面的是old hash 表,其中的Hash表的size=2, 加载阈值为2∗0.75=1,所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
③ 接下来的三个步骤是Hash表 resize成4,然后所有的 重新rehash的过程
正常的Rehash的过程:
并发下的Rehash过程:
1)假设我们有两个线程,用红色和浅蓝色标注了一下。
我们再回头看一下transfer代码中的这个细节:
do { Entry next = e.next; //假设线程一执行到这里就被调度挂起了 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next;} while (e != null);
而我们的线程二执行完成了。于是我们有下面的这个样子。
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转了。
2)线程一被调度回来执行
先是执行 newTalbe[i] = e;然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)
3)一切安好
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
4)环形链表出现
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了Infinite Loop。
有人把这个问题报给了Sun,不过Sun不认为这是一个问题。因为HashMap本来就不支持并发,要并发就用ConcurrentHashmap。
这个循环链表问题只存在于JDK1.7中,在JDK1.8中使用了不同的扩容实现方式,所以不会出现这种情况。JDK1.8中HashMap是如何实现的我们后续讲解。
欢迎小伙伴们留言交流~~
hashcode是什么意思_面试官:说一下HashMap原理,为什么会产生死循环相关推荐
- hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵
这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...
- 被替换的项目不是替换值长度的倍数_面试官,为啥HashMap的长度是2的n次方?
前言 HashMap的主干是一个数组,假设我们有3个键值对dnf:1,cf:2,lol:3,每次放的时候会根据hash函数来确定这个键值对应该放在数组的哪个位置,即index = hash(key) ...
- eui加载时间长_面试官:为什么 HashMap 的加载因子是0.75?
有很多东西之前在学的时候没怎么注意,笔者也是在重温HashMap的时候发现有很多可以去细究的问题,最终是会回归于数学的,如HashMap的加载因子为什么是0.75? 本文主要对以下内容进行介绍: 为什 ...
- redis查看key的过期时间_面试官:你在Redis中设置过带过期时间的Key吗?
点击上方小伟后端笔记关注公众号 每天阅读Java干货文章 熟悉Redis的同学应该知道,Redis的每个Key都可以设置一个过期时间,当达到过期时间的时候,这个key就会被自动删除. 在为key设置过 ...
- redis list设置过期时间_面试官:你在Redis中设置过带过期时间的Key吗?
点击上方小伟后端笔记关注公众号 每天阅读Java干货文章 熟悉Redis的同学应该知道,Redis的每个Key都可以设置一个过期时间,当达到过期时间的时候,这个key就会被自动删除. 在为key设置过 ...
- 引用计数器法 可达性分析算法_面试官:你说你熟悉jvm?那你讲一下并发的可达性分析...
持续输出原创文章,点击蓝字关注我吧 上面这张图是我还是北漂的时候,在鼓楼附近的胡同里面拍的. 那天刚刚下完雨,路过这个地方的时候,一瞬间就被这五颜六色的门板和自行车给吸引了,于是拍下了这张图片.看到这 ...
- 面试java你最擅长什么_面试官最喜欢问的10道Java面试题
1.Java的HashMap是如何工作的? HashMap是一个针对数据结构的键值,每个键都会有相应的值,关键是识别这样的值. HashMap 基于 hashing 原理,我们通过 put ()和 g ...
- java录排名怎么写_面试官:Java排名靠前的工具类你都用过哪些?
你知道的越多,不知道的就越多,业余的像一棵小草! 你来,我们一起精进!你不来,我和你的竞争对手一起精进! 编辑:业余草 推荐:https://www.xttblog.com/?p=5158 在Java ...
- redis删除过期key的算法_面试官别再问我Redis内存满了该怎么办了
概述 Redis的文章,我之前写过一篇关于「Redis的缓存的三大问题」,累计阅读也快800了,对于还只有3k左右的粉丝量,能够达到这个阅读量,已经是比较难了. 这说明那篇文章写的还过得去,收到很多人 ...
最新文章
- php类实例方法静态方法,PHP类中的静态方法使用实例
- centos7安装配置ELK(Elasticsearch+Logstash+Kibana)
- 立体匹配算法实现之:AdaptWeight
- 美媒:中国大陆最火的工作,教人工智能识图
- 计算机考试网络应用题一定要做到ie浏览器,网络远程教育统考单项练习:计算机应用基础之Internet应用部分(二)...
- 值传递,引用传递,指针传递
- SPARK学习之 --- eclipse / sbt / scala 配置
- rsync+inotify 实现数据实时同步
- 一文搞懂Java泛型到底是什么东东
- 【Ajax】后台验证用户输入的验证码是否与随机生成的验证码一直
- 声学计算机软件,常用声学仿真软件汇总
- java中的Properties配置文件
- C++卡常数之内存优化
- AMiner推荐论文:Strongly coupled N-doped graphene quantum dots/Ni(Fe)OxHy electrocatalysts with accelerat
- 积分商城系统业务逻辑思维导图_怎么开发积分商城系统_OctShop
- 【Linux】虚拟机VMware的Ubuntu使用vi指令的方向键和backspace空格键乱码
- LXC的网络结构和端口映射
- 国内外php商城系统 开源、php商城比较。
- Glide自定义缓存key
- Mac M1 nvm install失败问题
热门文章
- Oracle 非dba用户 使用 set autotrace 功能
- oracle rman 实例,Oracle数据库rman常用命令的使用示例
- 计算机应用第7章在线测试,《计算机应用基础》第07章在线测试
- java四种修饰符_java中的四种修饰符
- 基于JAVA+SpringMVC+Mybatis+MYSQL的田径运动会管理系统
- 极递云课显示服务器连接超时,服务器连接问题
- 苏州大学计算机组成题库11,苏州大学计算机组成题库(范文).doc
- java applet 换行_如何用java applet 画字符串,宽度大于设定值,自动换行
- Android BottomNavigationBar导航栏
- python的变量与注释