HashMap是Map接口的一种实现,实现key-value高效存储与检索。
Table of Contents
类继承实现
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
可知HashMap继承自AbstractMap,实现3个接口
核心变量
HashMap默认初始大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
容量最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
loadFactor决定HashMap扩容时机,当数据个数大于容量*loadFactor,则HashMap进行扩容。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient int size;
int threshold;
final float loadFactor;
与树形变换有关的变量,jdk1.7中每个HashMap数组元素中持有一个链表,jdk1.8中当某个数组元素长度大于TREEIFY_THRESHOLD,并且元素容量超过MIN_TREEIFY_CAPACITY时,则会将链表变形为二叉树。当某个数组元素树形结构元素个数小于UNTREEIFY_THRESHOLD时,则会执行将树形结构变换为链表结构。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
整个HashMap的核心存储结构 table,是一个数组,数组每个元素为Node类型
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
Node数据类型
Node作为HashMap中key-value数据的实际存储着,核心字段4个。其中next为Node类型,因此可以形成链表数据结构
final int hash;
final K key;
V value;
Node<K,V> next;
其代码如下所示 实现Map.Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
构造函数
构造函数一共4种,分别为无参构造,设置初始容量,设置初始容量和负载因子,从集合构造
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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;
this.threshold = tableSizeFor(initialCapacity);
}
主要研究一下指定初始容量以及负载因子构造函数public HashMap(int initialCapacity, float loadFactor)
判断初始容量是否合法,设置负载因子
根据设置的初始容量来求出一个刚好大于该容量的2的幂次方的数来作为最终初始容量,HashMap的容量都是2的幂次方形式。
tableSizeFor计算大于等于容量的最小2的幂次方
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;
}
给定一个数字,要计算比这个数字大的第一个2的幂次方,从数的二进制表示角度来看,2的幂次方数据有一个特点就是仅有一位是1.因此假定给定的数字二进制为00001001,现在要寻找第一个比这个数大的2幂次方数,即找到该二进制数的最高位1,将最高位到最低位二进制数全部变为1,即00001111,此时加一便得到00010000,此时便得到第一个比数大的2幂次方。
n |= n >>> 1;
实现最高位与次高位两位变为1,即从最高位开始连续两个1
n |= n >>> 2;
实现从最高位开始连续4个1,直到n |= n >>> 16会将所有位置为1.如果超出最大容量则返回最大容量,否则返回n+1.
借鉴一下大神的计算过程
核心方法put(K,V)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
将key进行hash散列
将key的hashcode值最高16位与最低16位异或
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将某一元素放入HashMap中,首先计算该元素属于数组元祖table哪个位置,然后将该元素放入到该位置的链表或树上去,计算方式如下:
n = (tab = resize()).length;
p = tab[i = (n - 1) & hash])
n表示HashMap容量大小为2的幂次方,hash表示key计算hashcode值,由于n-1形式为000000111111,与hash进行与运算,则hash只有低四位参与计算,因此这里将key值hashcode再次hash计算一次,减少key值冲突。
继续执行插入操作
putVal(hash(key), key, value, false, true);
HashMap在构造时并不会分配内存空间,执行第一次插入时会分配空间,首先判断是否是第一次插入:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
resize初始化
首先分析初始化时 oldCap=0 oldThr=0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
由于map原来为空,使用默认值newCap=16 newThr = 12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
分配Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
计算此时map容量
n = (tab = resize()).length;
计算该key在Node数组中的索引
p = tab[i = (n - 1) & hash]
如果该位置元素为空,说明该key-value是该数组位置的第一个元素,则直接新建一个Node放入该位置。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
如果该位置元素不为空,说明已经有元素,判断该元素key值与待插入key值是否相同
如果相同,值执行更新操作,返回旧值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
如果不相同,则将该key放入合适位置,分为两种情况,链表形式插入以及树形结构插入
树形插入:
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;
}
}
找到数组索引位置元素p,找到p的下一个元素e,
如果e==null,则说明数组该位置只有一个元素,则直接新建Node节点插入
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
如果数组该位置元素数量超过阈值TREEIFY_THRESHOLD - 1,则执行树形转换treeifyBin(tab, hash);
如果e!=null,则判断e key值与带插入key值是否相同,如果相同,则执行更新操作,返回。
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
否则继续执行遍历该位置下一个Node节点,直到遍历到链表末尾,新建Node放入。
如果e!=null,说明map中早已含有该key值,则执行更新操作。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
判断map是否应该扩容
if (++size > threshold)
resize();
resize扩容
此时oldCap=oldTab.length oldThr = threshold
newCap newThr变为原来2倍
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
分配新容量Node数组:
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
将原来数组的位置元素放入到新数组的响应位置中
遍历原来数组每一个数组元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
取出每个位置的第一个Node元素e,如果e.next==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);
如果为链表,则执行
扩容过程中链表再散列
将链表的每一个元素放入到新数组的适当位置中,原索引或者原索引+原数组容量
由于map容量为2的幂次方,数组索引计算方式为(n-1)&hash,新扩容之后的数组长度为2*n,新的坐标索引为(2n-1)&hash。2的幂次方表现为二进制中只有一个1,假设第N位为1,其余为0,则数组索引所标只与hash的低n-1位有关,扩容之后新数组坐标索引只有hash的低n位有关。如果n&hash=0,则说明hash值第N为0,则扩容后新数组索引与旧数组一致;如果n&hash=1,说明hash值第N位为1,则新扩容后的索引为(n-1)&hash+n。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
由于原来数组元素必在新数组原索引或者原索引+原容量位置处,因此将旧数组该位置处链表分为两个链表,原索引以及新索引。
loHead指向原索引头 loTail指向原索引尾 hiHead 新索引头 hiTail 新索引尾
旧索引链表更新
第一个元素赋值loHead=e,新元素插入到链表末尾loTail.next=e,更新loTail指针 loTail=e
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
新索引链表更新:
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
将旧新链表分别放入到新数组中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
链表扩容再散列过程
核心方法get
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
根据hash(key)确定该key应该在数组中的索引位置(n - 1) & hash
获取该位置第一个元素,判断该元素key是否等于待查询key,如果相同则直接返回。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
如果不相同,且该节点为树形节点TreeNode,则执行树形结构查找getTreeNode
否则为普通的链表的查找,Node节点hash值与hash(key)相同,并且,Node key与key相同。
接下来为树形结构操作方法
HashMap结构如下:
最基本是一个Node类型数组,每个数组位置维持一个链表或者红黑树结构。
研究一下TreeNode数据结构
TreeNode数据结构
基本的数据结构如下:继承自LinkedHashMap.Entry结构
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
LinkedHashMap.Entry 可知最终是HashMap.Node子类
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
红黑树的特性
首先是一颗二叉排序树,其次
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。[这里指到叶子节点的路径]
树形转换treeifyBin
在执行putVal方法时,如果要数组table某个位置处链表长度大于阈值,则将该链表转换为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
如果table数组长度太小,则不会执行树化,而会执行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
如果数组长度大于阈值,并且链表长度大于阈值,则执行树化,在树化之前需要遍历链表的每一个节点将Node类型转化为TreeNode类型
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);
取出链表的第一个元素即table数组中元素e,将e转换为TreeNode
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
由于TreeNode有prev指针,需要进行指针赋值
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);
转换过程如下:
转换为TreeNode数组之后,然后由TreeNode数组构建红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
构建红黑树treeify
遍历TreeNode数组的每一个元素,如果root==null,说明当前元素应该为红黑树的根
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
如果root!=null,说明红黑树已经构建一部分,则需要将该节点插入到该红黑树的合适部分,然后执行调整操作,使插入后节点不破坏红黑树的性质。利用hash值由根开始比较,如果小于则进入左子树,如果大于则进入右子树。
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
如果hash值相等,则比较key值
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
如果key值没有实现Comparable接口,comparableClassFor如果x实现Comparable接口则返回该类型,否则返回null
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
如果key值实现Comparable接口,则比较key值
dir = compareComparables(kc, k, pk)
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
如果key值也相等,则使用下面规则来比较
dir = tieBreakOrder(k, pk);
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
将待插入节点从根开始依次比较,直到找到最后一个节点,此时xp指向最后一个节点 p==null
dir=-1,表示左子树 dir=1表示右子树,将指针赋值指向。
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
最后由于执行插入该节点,破坏红黑树性质,需要进行调整。
root = balanceInsertion(root, x);
具体的调整方式,后面再具体研究一下。
最后将root放入到table[index]中作为根来检索
moveRootToFront(tab, root);
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
具体来说,将root的前驱与后继相连接,将root放置在first前面。
反树化untreeify
由于数组table某一位置元素个数太少,将红黑树结构转化为链表结构
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
根据TreeNode节点新建Node节点,将相应指针赋值
树形插入putTreeVal
向红黑树中插入一个TreeNode
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
找到红黑树的根,如果当前节点parent==null,说明当前节点为根,否则沿parent指针向上回溯
TreeNode<K,V> root = (parent != null) ? root() : this;
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
找到根元素后,从根元素开始寻找,利用hash值进行比较,如果小于则进入左子树,大于则进入右子树。
如果hash值相等,并且key值也相等,说明key值已存在,则执行更新操作,直接返回该节点。
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
如果key值没有实现Comparable接口,或者key值相等,则首先检查是否红黑树已经含有该节点,否则利用特定规则来比较。
执行插入之前,一定要确定key是否存在,如果已经存在则执行更新操作,如果不存在执行新建操作。
比较规则是先比较hash值,然后比较key值,最后按照特定规则来比较。
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
找到待插入节点位置,新建节点,指针赋值,执行平衡操作。
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
指针调整如下图所示:
扩容树形再散列split
在进行扩容时,原数组为红黑树时,进行扩容时,再次散列,需要将原数组位置红黑树进行拆分,放置在新数组合适位置,由于原数组红黑树进行拆分,导致红黑树元素个数减小,如果小于阈值,则需要执行反树化,将红黑树转化为列表。
由于原数组元素位置index必在新数组index或者index+table.length位置处,遍历红黑树将红黑树转换为旧链表以及新链表
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
旧链表,判断是否需要树化或者反树化,放置在新数组index位置
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
新链表,判断是否需要树化或者反树化,放置在新数组index+旧数组容量位置
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
树形查找getTreeNode
从红黑树中查找某一key
首先找到根节点,调用find方法
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
利用hash,key以及特定比较规则来进行比较,如果小于进入左子树,如果大于进入右子树。
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
核心方法remove
从map中删除某一key
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
首先找到该key在数组table中索引位置,找到第一个元素。如果第一个元素使要寻找的key值,则标记已经找到。
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
如果该位置处为红黑树结构,则首先在红黑树中找到该key值
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
如果该位置处是链表结构,则在链表中查找该元素
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
如果已经找到该元素,则从红黑树中删除或者从链表中删除
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
从红黑树中删除一个元素
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
还有红黑树的一系列辅助方法以后研究
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
JDK1.7中扩容并发死锁问题
JDK1.7中当并发扩容时会出现死锁问题,首先研究一下JDK1.7中扩容方法
put方法源码
public V put(K key, V value)
{
......
//算Hash值
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//如果该key已被插入,则替换掉旧的value (链接操作)
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++;
//该key不存在,需要增加一个结点
addEntry(hash, key, value, i);
return null;
}
如果key已存在更新操作,如果key不存在则执行新建操作addEntry
void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
if (size++ >= threshold)
resize(2 * table.length);
}
如果当前size超过阈值则执行扩容操作
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//创建一个新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//将Old Hash Table上的数据迁移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
创建一个新的数组,将旧数组元素迁移到新数组中去
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
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);
}
}
}
遍历旧数组每一个位置,遍历每一个位置的链表,将每个元素都重新计算一下tableIndex,然后放入到新数组相应位置
正常单线程resize过程
假定table大小为2,index=key%table.size。初始时三个元素2,7,5均在1位置处,现在进行扩容,扩容之后table.size=4
并发情况下resize过程
假定两个线程同时对于map进行扩容
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);
线程1在next=e.next停住,线程2扩容执行完毕
线程1继续执行,
- 先是执行 newTalbe[i] = e;
- 然后是e = next,导致了e指向了key(7),
- 而下一次循环的next = e.next导致了next指向了key(3)
把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
此时出现环形链表,get方法会死锁。
Jdk 1.8以前,导致死循环的主要原因是扩容后,节点的顺序会反掉,如下图:扩容前节点A在节点C前面,而扩容后节点C在节点A前面。
JDK1.7扩容时,是对于数组每一个位置的每一个链表依次遍历,将旧元素放入到新数组的适当位置。
JDK1.8扩容时,是遍历数组的每一个位置,判断为链表还是红黑树。假设为链表,当将该链表分成两个链表,旧索引链表以及新索引链表,然后将各个链表连接到新数组合适位置。
JDK1.8扩容:
可以看出,扩容后,节点A和节点C的先后顺序跟扩容前是一样的。因此,即使此时有多个线程并发扩容,也不会出现死循环的情况。
参考地址:
https://blog.csdn.net/fan2012huan/article/details/51097331