大热的《阿里巴巴Java开发规约》中有提到:html
【推荐】集合初始化时,指定集合初始值大小。java
说明:HashMap使用以下构造方法进行初始化,若是暂时没法肯定集合大小,那么指定默认值(16)便可:算法
public HashMap (int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}数组
看到代码规约这一条的时候,我以为是否是有点太 low 了,身为开发,你们都知道 HashMap 的原理。性能优化
什么?这个要经过插件监测?不必吧,哪一个开发不知道默认大小,什么时候 resize 啊,而后我和孤尽打赌随机咨询几位同窗如下几个问题:数据结构
HashMap 默认bucket数组多大?多线程
若是new HashMap<>(19),bucket数组多大?并发
HashMap 何时开辟bucket数组占用内存?app
HashMap 什么时候扩容?dom
抽样调查的结果出乎个人意料:
HashMap 默认bucket数组多大?(答案是16,大概一半的同窗答错)
若是new HashMap<>(19),bucket数组多大?(答案是32,大多被咨询同窗都不太了解这个点)
HashMap 何时开辟bucket数组占用内存?(答案是第一次 put 时,一半同窗认为是 new 的时候)
HashMap 什么时候扩容?(答案是put的元素达到容量乘负载因子的时候,默认16*0.75,有1/4同窗中枪)
HashMap 是写代码时最经常使用的集合类之一,看来你们也不是全都很了解。孤尽乘胜追击又抛出问题:JDK8中 HashMap 和以前 HashMap 有什么不一样?
我知道 JDK8 中 HashMap 引入了红黑树来处理哈希碰撞,具体细节和源代码并无仔细翻过,看来是时候对比翻看下 JDK8 和 JDK7 的 HashMap 源码了。
经过对比翻看源码,先说下结论:
HashMap 在 new 后并不会当即分配bucket数组,而是第一次 put 时初始化,相似 ArrayList 在第一次 add 时分配空间。
HashMap 的 bucket 数组大小必定是2的幂,若是 new 的时候指定了容量且不是2的幂,实际容量会是最接近(大于)指定容量的2的幂,好比 new HashMap<>(19),比19大且最接近的2的幂是32,实际容量就是32。
HashMap 在 put 的元素数量大于 Capacity * LoadFactor(默认16 * 0.75) 以后会进行扩容。
JDK8在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构,提升了性能。
JDK8在 resize 的时候,经过巧妙的设计,减小了 rehash 的性能消耗。
存储结构
JDK7 中的 HashMap 仍是采用你们所熟悉的数组+链表的结构来存储数据。
JDK8 中的 HashMap 采用了数组+链表或树的结构来存储数据。
重要参数
HashMap中有两个重要的参数,容量(Capacity) 和 负载因子(Load factor)
Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
Initial capacity 决定 bucket 的大小,Load factor 决定 bucket 内数据填充比例,基于这两个参数的乘积,HashMap 内部由 threshold 这个变量来表示 HashMap 能放入的元素个数。
Capacity 就是 HashMap 中数组的 length
loadFactor 通常都是使用默认的0.75
threshold 决定能放入的数据量,通常状况下等于 Capacity * LoadFactor
以上参数在 JDK7 和 JDK8中是一致的,接下来会根据实际代码分析。
JDK8 中的 HashMap 实现
new
HashMap 的bucket数组并不会在new 的时候分配,而是在第一次 put 的时候经过 resize() 函数进行分配。
JDK8中 HashMap 的bucket数组大小确定是2的幂,对于2的幂大小的 bucket,计算下标只须要 hash 后按位与 n-1,比%模运算取余要快。若是你经过 HashMap(int initialCapacity) 构造器传入initialCapacity,会先计算出比initialCapacity大的 2的幂存入 threshold,在第一次 put 的 resize() 初始化中会按照这个2的幂初始化数组大小,此后 resize 扩容也都是每次乘2,这么设计的缘由后面会详细讲。
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);
this.loadFactor = loadFactor;
// 比initialCapacity大的2的 N 次方,先存在threshold中,resize() 中会处理
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
hash
JKD8 中put 和 get 时,对 key 的 hashCode 先用 hash 函数散列下,再计算下标:
具体 hash 代码以下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
因为 h>>>16,高16bit 补0,一个数和0异或不变,因此 hash 函数大概的做用就是:高16bit不变,低16bit和高16bit作了一个异或,目的是减小碰撞。
按照函数注释,由于bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,若是不作 hash 处理,至关于散列生效的只有几个低 bit 位,为了减小散列的碰撞,设计者综合考虑了速度、做用、质量以后,使用高16bit和低16bit异或来简单处理减小碰撞,并且 JDK8中用了复杂度 O(logn)的树结构来提高碰撞下的性能。具体性能提高能够参考Java 8:HashMap的性能提高
put
put函数的思路大体分如下几步:
对key的hashCode()进行hash后计算数组下标index;
若是当前数组table为null,进行resize()初始化;
若是没碰撞直接放到对应下标的bucket里;
若是碰撞了,且节点已经存在,就替换掉 value;
若是碰撞后发现为树结构,挂载到树上。
若是碰撞后为链表,添加到链表尾,并判断链表若是过长(大于等于TREEIFY_THRESHOLD,默认8),就把链表转换成树结构;
数据 put 后,若是数据量超过threshold,就要resize。
具体代码以下:
public V put(K key, V value) {
// 对key的hashCode()作hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 初始 tab 为 null,resize 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算下标index,没碰撞,直接放
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 碰撞,节点已经存在
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 碰撞,树结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 碰撞,链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表过长,转换成树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 节点已存在,替换value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超过threshold,进行resize扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 数量大于 64 才会发生转换,避免初期,多个键值对刚好放入同一个链表中而致使没必要要的转化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
resize
resize()用来第一次初始化,或者 put 以后数据超过了threshold后扩容,resize的注释以下:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
数组下标计算: index = (table.length - 1) & hash ,因为 table.length 也就是capacity 确定是2的N次方,使用 & 位运算意味着只是多了最高位,这样就不用从新计算 index,元素要么在原位置,要么在原位置+ oldCapacity。
若是增长的高位为0,resize 后 index 不变,如图所示:
若是增长的高位为1,resize 后 index 增长 oldCap,如图所示:
这个设计的巧妙之处在于,节省了一部分从新计算hash的时间,同时新增的一位为0或1的几率能够认为是均等的,因此在resize 的过程当中就将原来碰撞的节点又均匀分布到了两个bucket里。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// oldCap > 0,是扩容而不是初始化
if (oldCap > 0) {
// 超过最大值就再也不扩充了,而且把阈值增大到Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// oldCap 为0,oldThr 不为0,第一次初始化 table,通常是经过带参数的构造器
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// oldCap 和 oldThr 都为0的初始化,通常是经过无参构造器生成,用默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 若是到这里 newThr 还没计算,则计算新的阈值threshold
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每一个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引(e.hash 和相似 0010000 按位与,结果为0,说明原高位为0)
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
JDK7 中的 HashMap 实现
new
JDK7 里 HashMap的bucket数组也不会在new 的时候分配,也是在第一次 put 的时候经过 inflateTable() 函数进行分配。
JDK7中 HashMap 的bucket数组大小也必定是2的幂,一样有计算下标简便的优势。若是你经过 HashMap(int initialCapacity) 构造器传入initialCapacity,会先存入 threshold,在第一次 put 时调用 inflateTable() 初始化,会计算出比initialCapacity大的2的幂做为初始化数组的大小,此后 resize 扩容也都是每次乘2。
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);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// ···省略代码
}
// 第一次 put 时,初始化 table
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// threshold 在不超过限制最大值的前提下等于 capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
// 接下来hash 部分分析
initHashSeedAsNeeded(capacity);
}
hash
JKD7 中,bucket数组下标也是按位与计算,可是 hash 函数与 JDK8稍有不一样,代码注释以下:
Retrieve object hash code and applies a supplemental hash function to the result hash, which defends against poor quality hash functions. This is critical because HashMap uses power-of-two length hash tables, that otherwise encounter collisions for hashCodes that do not differ in lower bits. Note: Null keys always map to hash 0, thus index 0.
hash为了防止只有 hashCode() 的低 bit 位参与散列容易碰撞,也采用了位移异或,只不过不是高低16bit,而是以下代码中屡次位移异或。
JKD7的 hash 中存在一个开关:hashSeed。开关打开(hashSeed不为0)的时候,对 String 类型的key 采用sun.misc.Hashing.stringHash32的 hash 算法;对非 String 类型的 key,多一次和hashSeed的异或,也能够必定程度上减小碰撞的几率。
JDK 7u40之后,hashSeed 被移除,在 JDK8中也没有再采用,由于stringHash32()的算法基于MurMur哈希,其中hashSeed的产生使用了Romdum.nextInt()实现。Rondom.nextInt()使用AtomicLong,它的操做是CAS的(Compare And Swap)。这个CAS操做当有多个CPU核心时,会存在许多性能问题。所以,这个替代函数在多核处理器中表现出了糟糕的性能。
具体hash 代码以下所示:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* 下标计算依然是使用按位与
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
hashSeed 默认值是0,也就是默认关闭,任何数字与0异或不变。hashSeed 会在capacity发生变化的时候,经过initHashSeedAsNeeded()函数进行计算。当capacity大于设置值Holder.ALTERNATIVE_HASHING_THRESHOLD后,会经过sun.misc.Hashing.randomHashSeed产生hashSeed 值,这个设定值是经过 JVM的jdk.map.althashing.threshold参数来设置的,具体代码以下:
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
/**
* holds values which can't be initialized until after VM is booted.
*/
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;// ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
put
JKD7 的put相比于 JDK8就要简单一些,碰撞之后只有链表结构。具体代码以下:
public V put(K key, V value) {
// 初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
// 计算 hash 和下标
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 查到相同 key,替换 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++;
// 添加数据
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 若是数据量达到threshold,须要 resize 扩容
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] = new Entry<>(hash, key, value, e);
size++;
}
resize
JDK7的 resize() 也是扩容两倍,不过扩容过程相对JDK8就要简单许多,因为默认initHashSeedAsNeeded内开关都是关闭状态,因此通常状况下transfer 不须要进行 rehash,能减小一部分开销。代码以下所示:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// 扩容后转移数据,根据initHashSeedAsNeeded判断是否 rehash
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
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;
// 根据以前的分析,默认状况是不须要 rehash 的
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;
}
}
}
总结
HashMap 在 new 后并不会当即分配bucket数组,而是第一次 put 时初始化,相似 ArrayList 在第一次 add 时分配空间。
HashMap 的 bucket 数组大小必定是2的幂,若是 new 的时候指定了容量且不是2的幂,实际容量会是最接近(大于)指定容量的2的幂,好比 new HashMap<>(19),比19大且最接近的2的幂是32,实际容量就是32。
HashMap 在 put 的元素数量大于 Capacity * LoadFactor(默认16 * 0.75) 以后会进行扩容。
JDK8处于提高性能的考虑,在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构。
JDK8在 resize 的时候,经过巧妙的设计,减小了 rehash 的性能消耗。
相对于 JDK7的1000余行代码,JDK8代码量达到了2000余行,对于这个你们最经常使用的数据结构增长了很多的性能优化。
仔细看完上面的分析和源码,对 HashMap 内部的细节又多了些了解,有空的时候仍是多翻翻源码,^_^
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,这个成本至关的大。
一、多线程put操做后,get操做致使死循环。
二、多线程put非NULL元素后,get操做获得NULL值。
三、多线程put操做,致使元素丢失。
为什么出现死循环?
你们都知道,HashMap采用链表解决Hash冲突,具体的HashMap的分析能够参考一下Java集合—HashMap源码剖析 的分析。由于是链表结构,那么就很容易造成闭合的链路,这样在循环的时候只要有线程对这个HashMap进行get操做就会产生死循环。可是,我好奇的是,这种闭合的链路是如何造成的呢。在单线程状况下,只有一个线程对HashMap的数据结构进行操做,是不可能产生闭合的回路的。那就只有在多线程并发的状况下才会出现这种状况,那就是在put操做的时候,若是size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操做,随之HashMap的结构就会发生翻天覆地的变化。颇有可能就是在两个线程在这个时候同时触发了rehash操做,产生了闭合的回路。
下面咱们从源码中一步一步地分析这种回路是如何产生的。先看一下put操做:
存储数据put
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
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++;
addEntry(hash, key, value, i);
return null;
}
当咱们往HashMap中put元素的时候,先根据key的hash值获得这个元素在数组中的位置(即下标),而后就能够把这个元素放到对应的位置中了。 若是这个元素所在的位置上已经存放有其余元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,而先前加入的放在链尾。
检查容量是否超标addEntry
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);
}
能够看到,若是如今size已经超过了threshold,那么就要进行resize操做,新建一个更大尺寸的hash表,而后把数据从老的Hash表中迁移到新的Hash表中:
调整Hash表大小resize
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
当table[]数组容量较小,容易产生哈希碰撞,因此,Hash表的尺寸和容量很是的重要。通常来讲,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,若是超过,须要增大Hash表的尺寸,这个过程称为resize。
多个线程同时往HashMap添加新元素时,屡次resize会有必定几率出现死循环,由于每次resize须要把旧的数据映射到新的哈希表,这一部分代码在HashMap#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部分代码是致使多线程使用hashmap出现CUP使用率骤增,从而多个线程阻塞的罪魁祸首。
HashMap在并发执行put操做时会引发死循环, 是由于多线程会致使 HashMap的 Entry链表造成环形数据结构, 一旦造成环形数据结构, Entry 的 next 节点永远不为空, 就会产生死循环获取 Entry。