HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生成一个数字作为数组下标,这个数字就是hashcode)所以在查询上的访问速度比较快,HashMap最多允许一对键值对的Key为Null,允许多对键值对的value为Null。它的线程不是安全的,在排序上面是无序的。
//Node可以看做就是一个节点,多个Node节点构成链表,当链表长度大于8的时候转换为红黑树。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树形化阈值,当链表节点个大于等于TREEIFY_THRESHOLD - 1时,会将该链表换成红黑树
static final int TREEIFY_THRESHOLD = 8;
//解除树形化阈值,当链表节点小于等于这个值时,会将红黑树转换成普通的链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化的容量,即:当内部数组长度小于64时,不会将链表转化成红黑树,而是优先扩充数组
static final int MIN_TREEIFY_CAPACITY = 64;
//hashmap的内部数组,Node则是链表节点对象
transient Node<K,V>[] table;
//容器类成员
transient Set<Map.Entry<K,V>> entrySet;
//元素个数
transient int size;
//容器结构的修改次数
transient int modCount;
//阈值,超过这个值时扩充数组。threshold = capacity * load factor
int threshold;
//负载因子
final float loadFactor;
//默认数组初始容量为16,负载因子为0.75f
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定具体的初始容量
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);
this.loadFactor = loadFactor;
//tableSizeFor方法返回一个比给定整数大且最接近的2的幂次方整数
this.threshold = tableSizeFor(initialCapacity);
}
//进入tableSizeFor方法
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;
}
/**原理实际上就是补位,将原本为0的空位填补为1,最后加1时,最高有效位进1,其余变为0
* int n = cat - 1就是防止cap已经是2的幂,执行后面无符号操作时,返回的capacity是这个cap * 的两倍。
*/
//假设传进来的cap值是10,n = cap -1 = 9
// 0000 1001 >>> 1 右移运算为 0000 0100 最后与运算 0000 1101
// 0000 1101 >>> 2 右移运算为 0000 0011 最后与运算 0000 1111
// 0000 1111 >>> 4 右移运算为 0000 0000 最后与运算 0000 1111
// 0000 1111 >>> 8 右移运算为 0000 0000 最后与运算 0000 1111
// 0000 1111 >>> 16 右移运算为 0000 0000 最后与运算 0000 1111
// n = n + 1 = 16
//得到的这个capacity却赋值给了threshold。但按实际应该这么写:this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;但是在构造方法中,并没有对table这个成员变量进行初始化,反而被推迟到了put方法中,在put方法中对threshold重新计算。
//构造一个和指定map有相同mappings的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//将m中所有的元素存入HashMap中
putMapEntries(m, false);
}
//进入putMapEntries方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取m中元素的个数
int s = m.size();
//当m中有元素时
if (s > 0) {
//判断table是否已经初始化,如果为初始化则先初始化一些变量
if (table == null) { // pre-size
//得到最小应设置的容量
float ft = ((float)s / loadFactor) + 1.0F;
//判断是否超过最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//把要创建的HashMap的容量存放到threshold中
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果已经初始化,判断map的size是否大于threshold,如果大于则进行resize()方法扩容
else if (s > threshold)
resize();
//遍历map中的元素
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//元素插入
putVal(hash(key), key, value, false, evict);
}
}
}
//进入扩容方法resize
final Node<K,V>[] resize() {
//保存当前table
Node<K,V>[] oldTab = table;
//保存当前table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//保存当前阈值
int oldThr = threshold;
//定义新的table的容量和阈值
int newCap, newThr = 0;
//判断原table的容量是否大于0,若大于0则代表原来的table表非空
if (oldCap > 0) {
//若旧table的容量大于最大容量,更新阈值为Integer.MAX_VALUE,这样以后不会自动扩容
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
}
//如果数组还没创建,但是已经指定了threshold(这种情况是带参构造创建的对象),threshold的值为数组长度
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//这种情况是通过无参构造创建的对象
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//可能是上面newThr = oldThr << 1时,最高位被移除了,变为0
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"})
//初始化table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//将原来数组的元素转移到新数组中
if (oldTab != null) {
//遍历原数组
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;
//如果该节点是红黑树,执行split方法,和链表类似的处理
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//此时节点是链表
else { // preserve order
// loHead,loTail为原链表的节点,索引不变
Node<K,V> loHead = null, loTail = null;
// hiHeadm, hiTail为新链表节点,原索引 + 原数组长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
next = e.next;
//最高位==0,这是索引不变的链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//最高位==1,这是索引发生改变的链表
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原链表存回原索引位
if (loTail != null) {
// 链表最后得有个null
loTail.next = null;
// 链表头指针放在新桶的相同下标(j)处
newTab[j] = loHead;
}
//新链表存到:原索引位 + 原数组长度
if (hiTail != null) {
hiTail.next = null;
//后节点新的位置一定为原来基础上加上oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//元素插入putval方法
// onlyIfAbsent:当存入键值对时,如果该key已存在,是否覆盖它的value。false为覆盖,true为不覆盖。
// evict:用于子类LinkedHashMap。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab:内部数组
// p:hash对应的索引位中的首节点
// n:内部数组的长度
// i:hash对应的索引位
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 首次put时,内部数组为空,扩充数组。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算数组索引,获取该索引位置的首节点,如果为null,添加一个新的节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果首节点的key和要存入的key相同,那么直接覆盖value的值
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);
// 此时首节点为链表,如果链表中存在该键值对,直接覆盖value。
// 如果不存在,则在末端插入键值对。然后判断链表是否大于等于7,尝试转换成红黑树。
// 注意此处使用“尝试”,因为在treeifyBin方法中还会判断当前数组容量是否到达64,
// 否则会放弃次此转换,优先扩充数组容量。
else {
// 走到这里,hash碰撞了。检查链表中是否包含key,或将键值对添加到链
for (int binCount = 0; ; ++binCount) {
// p.next == null,到达链表末尾,添加新节点,如果长度足够,转换成树结构。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 检查链表中是否已经包含key
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;
// 如果元素个数大于阈值,扩充数组。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
总结来说就是以下几个步骤:
1.检查数组是否为空,执行resize()扩充;
2.通过hash值计算数组索引,获取该索引位的首节点。
3.如果首节点为null,直接添加节点到该索引位。
4.如果首节点不为null,那么有3种情况
① key和首节点的key相同,覆盖value;否则执行②或③
② 如果首节点是红黑树节点(TreeNode),将键值对添加到红黑树。
③ 如果首节点是链表,将键值对添加到链表。添加之后会判断链表长度是否到达TREEIFY_THRESHOLD - 1这个阈值,“尝试”将链表转换成红黑树。
5.最后判断当前元素个数是否大于threshold,扩充数组。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//hash方法的作用是将hashCode进一步的混淆,增加其“随机度”,试图减少插入hashmap时的hash冲突,换句更专业的话来说就是提高离散性能。
//在putVal方法中(不仅仅只在putVal中),有这么一行代码
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//i = (n - 1) & hash,n是数组长度,hash就是通过hash()方法进行高低位异或运算得出来的hash值。这个表达式就是hash值的取模运算,上面已经说过当除数为2的次方时,可以用与运算提高性能。那么我们想想,大多数情况下,内部数组的容量一般都不会很大,基本分布在16~256之间。所以一个32位的hashCode,一直都用最低的4到8位进行与运算,而高位几乎没有参与。所以通过hash()方法,将hashCode高16位与低16位进行异或运算,能有效的提高离散性能。
1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)。
我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。
如果在取链表的时候从头开始取(现在是从尾部开始取)的话,则可以保证节点之间的顺序,那样就不存在这样的问题了。