前言

JDK1.7&1.8源码对比分析【集合】HashMap中我们遗留了一个问题:为什么HashMap在调用resize() 方法时会出现死循环?这篇文章就通过JDK1.7的源码来分析并解释这个问题。

如下,并发场景下使用HashMap造成Race Condition,从而导致死循环,现象是CPU 100%被占用。

final HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
map.put(UUID.randomUUID().toString(), "");
}
}).start();
}

目录

一、问题症状

二、Hash表数据结构

三、HashMap的rehash源代码

1. 正常的rehash的过程

2. 并发下的rehash过程

一、问题症状

我们在程序中会经常使用HashMap来存储键值对,在单线程场景下使用没有任何问题。当程序性能出现瓶颈,我们开始使用多线程来操作HashMap,但因此也带来了问题:发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。

我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

接下来我们分析一下具体的原因。

二、Hash表数据结构

HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。

我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是hash表的缺陷(可参看《Hash Collision DoS 问题》)。

所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大hash表的尺寸,但是这样一来,整个hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

三、HashMap的rehash源代码

下面,我们来看一下Java的HashMap的源代码。

put一个key,value对到hash表中:

public V put(K key, V value) {
// 判断当前数组是否需要初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 为空,则 put 一个空值进去
if (key == null)
return putForNullKey(value);
// 根据 key 计算出 hashcode
int hash = hash(key);
// 根据计算出的 hashcode 定位出所在桶
int i = indexFor(hash, table.length);
// 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 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 = value;
e.recordAccess(this);
return oldValue;
}
} modCount++;
// 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
addEntry(hash, key, value, i);
return null;
}

检查容量是否超标:

void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断是否需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} // 将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表
createEntry(hash, key, value, bucketIndex);
}

新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的hash表中:

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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

迁移的源代码,注意粗体部分:

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
// 从old table中取一个元素出来,然后放到new table中
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;
}
}
}

好了,这个代码算是比较正常的。而且没有什么问题。

1. 正常的rehash的过程

假设我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。

最上面的是old hash 表,其中的Hash表的size = 2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。

接下来的三个步骤是hash表 resize成4,然后所有的<key, value> 重新rehash的过程。

2. 并发下的rehash过程

2.1 假设我们有两个线程

我们再回头看一下我们的 transfer代码中的这个细节:

do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为线程一的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2.2 线程一被调度回来执行

先是执行 newTable[i] = e,然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)。

2.3 一切安好

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。

2.4 环形链接出现

e.next = newTable[i] 导致  key(3).next 指向了 key(7),注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

参考:疫苗:JAVA HASHMAP的死循环

JDK(九)JDK1.7源码分析【集合】HashMap的死循环的更多相关文章

  1. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  2. 【集合框架】JDK1.8源码分析之HashMap(一)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  3. 【集合框架】JDK1.8源码分析之HashMap

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  4. 【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)

    一.前言 在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析. 二.迭代器继 ...

  5. JDK1.8源码分析之HashMap(一) (转)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  6. JDK1.8源码分析之HashMap

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  7. 【集合框架】JDK1.8源码分析HashSet && LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  8. JDK(五)JDK1.8源码分析【集合】HashMap

    本文转载自无始无终,原文连接 HashMap 在 JDK 1.8 后新增的红黑树结构 传统 HashMap 的缺点 JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也 ...

  9. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  10. 集合之TreeSet(含JDK1.8源码分析)

    一.前言 前面分析了Set接口下的hashSet和linkedHashSet,下面接着来看treeSet,treeSet的底层实现是基于treeMap的. 四个关注点在treeSet上的答案 二.tr ...

随机推荐

  1. java多线程3种方式

    Java多线程实现方式主要有三种:继承Thread类.实现Runnable接口.使用ExecutorService.Callable.Future实现有返回结果的多线程.其中前两种方式线程执行完后都没 ...

  2. 关于vue2用vue-cli搭建环境后域名代理的http-proxy-middleware解决api接口跨域问题

    在vue中用http-proxy-middleware来进行接口代理,比如:本地运行环境为http://localhost:8080但真实访问的api为 http://www.baidu.com这时我 ...

  3. OpenGL学习--开发环境

    1. VS2017 Professional安装 1.1. 下载 mu_visual_studio_professional_2017_x86_x64_10049787.exe 1.2. 双击开始安装 ...

  4. 矩阵分解---QR正交分解,LU分解

    相关概念: 正交矩阵:若一个方阵其行与列皆为正交的单位向量,则该矩阵为正交矩阵,且该矩阵的转置和其逆相等.两个向量正交的意思是两个向量的内积为 0 正定矩阵:如果对于所有的非零实系数向量x ,都有 x ...

  5. MVP 模式简单易懂的介绍方式

    为什么用Android MVP 设计模式? 当项目越来越庞大.复杂,参与的研发人员越来越多的时候,MVP 模式 的优势就充分显示出来了. MVP 模式是 MVC 模式在 Android 上的一种变体, ...

  6. 网络基础 Windows下安装和配置net-snmp 代理

    Windows 下安装和配置net-snmp 代理[摘录] by:授客 QQ:1033553122   A.   安装  1.   安装前准备 ActivePerl-5.10.0.1004-MSWin ...

  7. 网络 互联网接入方法、Mbit与MB的转换

    ADSL:非对称数字用户环路(绝大多数家庭接入方法,使用电话线).可以提供最高1Mbps的上行速率和最高8Mbps的下行速率.最新的ADSL2+可以提供最高24Mbps的下行速率. 千千兆TB 千兆G ...

  8. redis介绍(6)集群(ruby)

    redis集群: redis集群是高可用的一种体现,让整个redis圈更加稳定,不易出现宕机的情况, redis原理: redis3.0之前是不支持集群的,实现集群要自己去配置实现,很麻烦,在3.0之 ...

  9. 【Java】解析xml

    xml: <?xml version="1.0" encoding="GB2312"?> <RESULT> <VALUE> ...

  10. docker 的简单使用

    运行一个ubuntu容器 咱们要在cenots7操作系统下,以docker下载一个ubuntu image文件,然后以image启动容器 docker pull ubuntu 或者指定版本:docke ...