从Java1.8源码角度剖析线程不安全的HashMap

HashMap的底层核心数据结构是什么?

HashMap<K, V>继承自AbstractMap<K, V>, 实现了Map接口,Cloneable,Serializable。
核心的存储结点是一个Node数组。大小是2的幂,除非是0。
transient Node<K,V>[] table;
其中的Node(Basic hash bin node, used for most entries)实现了Map.Entry接口,
主要包含了hash值,key, value, 和下一个Node的指针(同一slot有多个元素是用链式存储)。

HashMap包含哪些数据结构?

数组、链表、红黑树。

哈希槽(slot)的位置是如果确定的?如何避免散列冲突?

i = (n - 1) & hash
hash计算:int hash = hash(key);
hash函数:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //使h的高位参与运算,有助于泊松分布
 }

resize()是如何实现的?

Java1.8中resize函数是无参的。首先计算新的容量大小:

int oldThr = threshold;//The next size value at which to resize (capacity * load factor).
if (oldThr > 0) // initial capacity was placed in threshold
	newCap = oldThr;

然后循环对table数组中的Node做迁移工作,数组中没有数据的位置略过,只对有元素的位置做处理。先将结点存储在e中,再把该结点置为null,重新找位置存放e
如果该位置(既现在的e)只有一个结点:

if (e.next == null) newTab[e.hash & (newCap - 1)] = e;//还是用hash与数组长度来计算slot位置

如果该位置存放了多个结点,对原先链表的头尾结点引用,保证有序性(preserve order)。

为什么线程不安全?

从put()方法开始分析,put方法的实质是putValue()方法,第一个参数为hash(key)。
一、如果计算出来的哈希槽slot位置无元素,则新增结点:

if ((p = tab[i = (n - 1) & hash]) == null)
	tab[i] = newNode(hash, key, value, null);

二、否则,声明一个结点e (为了简化先不讨论TreeNode)
先比较slot位置的第一个结点哈希值再比较key值,如果都相等,将e指向该结点:

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//这里先比较hash值会加快key的比较速度。
	e = p;

如果不相等,再依次比较slot位置的后续结点,如果后续结点为空,则新增。

if ((e = p.next) == null) 
	p.next = newNode(hash, key, value, null);//此处如果多线程操作,会丢失结点

如果不为空,再比较key值,如果相同则停止,如果还不同,则再继续比较下一个。
以上操作的目的,是将e指向key应该存储的位置。如果该key处已经有旧的value则替换,返回旧的value。
如果添加元素超过了threshold,则进行resize()。从代码上看是先增加元素再扩容,而JDK7是先扩容再增加元素。如果在扩容的时候,正好有get操作,则取到的是旧槽的元素。

if (++size > threshold) resize();

下面回答为什么线程不安全的问题。通过以上分析,可以看到put方法用了e来临时存储结点,而且用了大量的指针的赋值操作。当多个线程并发执行的时候,容易出现指针错乱,造成结点丢失,或者出现循环链表。

什么时候会树化?

当某个槽内的元素增加到8个时,由链表转为红黑树;
当某个槽内的元素减少到6个时,由红黑树转为链表。

负载因子loadFactor有什么用?

默认值是0.75。当元素数量大于 size*loadFactor时进行扩容,这样既不会浪费过多资源,又可以减少散列冲突。

数组默认大小多少? 最大容量多少?如何初始化?

默认大小:1 << 4;
最大容量:1 << 30;

HashMap的缺点

HashMap的线程不安全,put和get操作容易造成死链问题,造成CPU占满,扩容有可能造成结点丢失。因此在多线程环境下使用ConcurrentHashMap, 下一篇文章介绍。

猜你喜欢

转载自blog.csdn.net/weixin_42628594/article/details/83933259