一、hashmap简介
hashmap是Java当中一种数据结构,是一个用于存储Key-Value键值对的集合,每一个键值对也叫作Entry。
二、JDK7的HashMap
1、JDK7时HashMap的数据结构
1、在JDK7之前,hashmap底层采用数组+链表的数据结构来存储数据
2、插入数据采用头插法,头插法效率更高,不需要去遍历链表。插入结点后将头结点移到数组下标的位置
什么是头插法?咱们看一副图你就了解了
每次都在头结点插入,其余的节点依次往下挪。那么这样就会造成一个问题,当扩容的时候,对每个节点重新rehash。假设重新计算hash后孙悟空和孙尚香依然还是在一条链上,但可能顺序变了,变成孙尚香—>孙悟空。而原来的孙悟空—>孙尚香这个指针又没有断开,这样就会形成环,最终会导致死锁
2、手写HashMap部分源码
public class MyHashMap<K,V>{
private Entry[] table;
private static Integer CAPACITY=8;
private int N; //元素个数
public int size(){
return N;
}
public get(K key){
int hash=key.hashCode(); //求出key的hashCode
int i=hash%8; //计算下标
for(Entry<K<V> en=table[i];en!=null;en=en.next){
if(entry.key.equals(key)){
return entry.v;
}
}
}
public V put(K key,V value){
int hash=key.hashCode(); //求出key的hashCode
int i=hash%8; //计算下标
//如果存在相同的key,则更新值
for(Entry<K<V> en=table[i];en!=null;en=en.next){
if(entry.key.equals(key)){
V oldValue=entry.v;
entry.v=value;
return oldValue;
}
}
Entry entry=new Entry(key,value,table[i]); //头插法
table[i]=entry; //将头结点放在数组的位置
N++;
return null;
}
//结点类
class Entry<K,V>{
private K key;
private V value;
private Entry<K,V> next;
public Entry(K key,V velue,Entry<K,V> next){
this.key=key;
this.value=value;
this.next=next;
}
public K getKey(){
return key;
}
public V getValue(){
return value;
}
}
}
3、相关问题解答
为什么hashmap的容量是2的幂次方?
1、&运算速度快,至少比%取模运算块
2、(n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n
3、采用(n - 1) & hash来计算索引,当n为2的幂次方的时候,(n-1)转换成为二进制保证低位全是1
为什么JDK7中hashmap源码计算hashcode要右移?
求索引位置时保证高位也能参与位运算。为了保证求出来的散列值均匀。如果计算出来的索引扎堆,那么这就不算一个好的哈希算法
JDK7hashmap如何判断是否需要扩容?
有两个条件。第一个是当前元素个数大于或等于阈值。第二个是当前数组table[i]!=null
Hashmap是线程不安全的?
1.多线程环境下,如果有多个线程同时对一个数据进行操作,很有可能出现数据覆盖的情况。
2.扩容有可能发生死锁情况
hashMap允许key为null。如果key为null,则默认把这个数据放在索引为0的位置处
三、JDK8的HashMap部分源码
1、jdk8中hashMap数据结构
1、JDK8以后hashmap的底层数据结构由数据+链表+红黑树实现
2、jdk8以后插入数据采用尾插法。因为引入了树形结构,总是要遍历的
当进行put操作的时候,当链表的长度大于或等于8时,会将链表转化为红黑树
在进行remove操作的时候,当红黑树的节点个数小于或者等于6时,会将红黑树转化为链表
2、相关问题解答
为什么使用红黑树而不使用其他的树形结构?
hashMmap中不仅存在查询,还存在修改的操作。红黑树的查询和修改效率处在链表和完全平衡二叉树之间
hashMap怎么设置初始值的大小
如果你在创建的时候没有设置初始值大小,那么它的默认容量是16。
如果你设置了一个初始容量,它会先进行一个判断,判断这个值是不是2的次幂,如果不是将会把容量转化成为2的次幂大小。比如说你设置的容量是27,那么创建的HashMap实际容量是32。
jdk7和jdk8中HashMap的区别
1、jdk8中当链表长度大于8时会将链表转化成为红黑树
2、节点插入顺序不同,jdk7采用头插法,而jdk8采用尾插法
3、hash算法的简化
在jdk7中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);
}
在jdk8中的hash算法为
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么在jdk8中要简化hash算法:jdk8之前之所以hash方法写的比较复杂,主要是为了提高散列行,进而提高遍历速度,但是jdk8以后引入红黑树后大大提高了遍历速度,继续采用复杂的hash算法也就没太大意义,反而还要消耗性能,因为不管是put()还是get()都需要调用hash()
4、扩容不同,在jdk7中,发生扩容,它会把原来所有的元素重新计算hash。再插入到新的位置。而jdk8中则是直接copy过去,要么位置不变,要么位置更改为索引+原数组长度
原文链接:https://blog.csdn.net/weixin_45119323/article/details/108659506
把书读薄
在Java 7中HashMap实现有1000多行,到了Java 8中增长为2000多行,虽然代码行数不多,但代码中有比较多的位运算,以及其他的一些细枝末节,导致这部分代码看起来很复杂,理解起来比较困难。但是如果我们跳出来看,HashMap这个数据结构是非常基础的,我们大脑中首先要有这样一幅图:
这张图囊括了HashMap中最基础的几个点:
- Java中HashMap的实现的基础数据结构是数组,每一对key->value的键值对组成Entity类以双向链表的形式存放到这个数组中
- 元素在数组中的位置由key.hashCode()的值决定,如果两个key的哈希值相等,即发生了哈希碰撞,则这两个key对应的Entity将以链表的形式存放在数组中
- 调用HashMap.get()的时候会首先计算key的值,继而在数组中找到key对应的位置,然后遍历该位置上的链表找相应的值。
当然这张图中没有体现出来的有两点:
- 为了提升整个HashMap的读取效率,当HashMap中存储的元素大小等于桶数组大小乘以负载因子的时候整个HashMap就要扩容,以减小哈希碰撞,具体细节我们在后文中讲代码会说到
- 在Java 8中如果桶数组的同一个位置上的链表数量超过一个定值,则整个链表有一定概率会转为一棵红黑树。
整体来看,整个HashMap中最重要的点有四个:初始化,数据寻址-hash方法,数据存储-put方法,扩容-resize方法,只要理解了这四个点的原理和调用时机,也就理解了整个HashMap的设计。
3. 把书读厚
在理解了HashMap的整体架构的基础上,我们可以试着回答一下下面的几个问题,如果对其中的某几个问题还有疑惑,那就说明我们还需要深入代码,把书读厚。
- HashMap内部的bucket数组长度为什么一直都是2的整数次幂
- HashMap默认的bucket数组是多大
- HashMap什么时候开辟bucket数组占用内存
- HashMap何时扩容?
- 桶中的元素链表何时转换为红黑树,什么时候转回链表,为什么要这么设计?
- Java 8中为什么要引进红黑树,是为了解决什么场景的问题?
- HashMap如何处理key为null的键值对?
3.1 new HashMap()
在JDK 8中,在调用new HashMap()的时候并没有分配数组堆内存,只是做了一些参数校验,初始化了一些常量
-
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);
-
}
-
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;
-
}
tableSizeFor的作用是找到大于cap的最小的2的整数幂,我们假设n(注意是n,不是cap哈)对应的二进制为000001xxxxxx,其中x代表的二进制位是0是1我们不关心,
n |= n >>> 1;执行后n的值为:
可以看到此时n的二进制最高两位已经变成了1(1和0或1异或都是1),再接着执行第二行代码:
可见n的二进制最高四位已经变成了1,等到执行完代码n |= n >>> 16;之后,n的二进制最低位全都变成了1,也就是n = 2^x - 1其中x和n的值有关,如果没有超过MAXIMUM_CAPACITY,最后会返回一个2的正整数次幂,因此tableSizeFor的作用就是保证返回一个比入参大的最小的2的正整数次幂。
在JDK 7中初始化的代码大体一致,在HashMap第一次put的时候会调用inflateTable计算桶数组的长度,但其算法并没有变:
-
// 第一次put时,初始化table
-
private void inflateTable(int toSize) {
-
// Find an power of 2 >= toSize
-
int capacity = roundUpToPowerOf2(toSize);
-
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
-
table = new Entry(capacity);
-
initHashSeedAsNeeded(capacity);
-
}
这里我们也回答了开头提出来的问题:
HashMap什么时候开辟bucket数组占用内存?答案是在HashMap第一次put的时候,无论Java 8还是Java 7都是这样实现的。这里我们可以看到两个版本的实现中,桶数组的大小都是2的正整数幂,至于为什么这么设计,看完后文你就明白了。
3.2 hash
在HashMap这个特殊的数据结构中,hash函数承担着寻址定址的作用,其性能对整个HashMap的性能影响巨大,那什么才是一个好的hash函数呢?
- 计算出来的哈希值足够散列,能够有效减少哈希碰撞
- 本身能够快速计算得出,因为HashMap每次调用get和put的时候都会调用hash方法
下面是Java 8中的实现:
-
static final int hash(Object key) {
-
int h;
-
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
-
}
这里比较重要的是(h = key.hashCode()) ^ (h >>> 16),这个位运算其实是将key.hashCode()计算出来的hash值的高16位与低16位继续异或,为什么要这么做呢?
我们知道hash函数的作用是用来确定key在桶数组中的位置的,在JDK中为了更好的性能,通常会这样写:
index =(table.length - 1) & key.hash();
回忆前文中的内容,table.length是一个2的正整数次幂,类似于000100000,这样的值减一就成了000011111,通过位运算可以高效寻址,这也回答了前文中提到的一个问题,HashMap内部的bucket数组长度为什么一直都是2的整数次幂?好处之一就是可以通过构造位运算快速寻址定址。
回到本小节的议题,既然计算出来的哈希值都要与table.length - 1做与运算,那就意味着计算出来的hash值只有低位有效,这样会加大碰撞几率,因此让高16位与低16位做异或,让低位保留部分高位信息,减少哈希碰撞。
我们再看Java 7中对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);
-
}
Java 7中为了避免hash值的高位信息丢失,做了更加复杂的异或运算,但是基本出发点都是一样的,都是让哈希值的低位保留部分高位信息,减少哈希碰撞。
3.3 put
在Java 8中put这个方法的思路分为以下几步:
- 调用key的hashCode方法计算哈希值,并据此计算出数组下标index
- 如果发现当前的桶数组为null,则调用resize()方法进行初始化
- 如果没有发生哈希碰撞,则直接放到对应的桶中
- 如果发生哈希碰撞,且节点已经存在,就替换掉相应的value
- 如果发生哈希碰撞,且桶中存放的是树状结构,则挂载到树上
- 如果碰撞后为链表,添加到链表尾,如果链表长度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构
- 数据put完成后,如果HashMap的总数超过threshold就要resize
具体代码以及注释如下:
-
public V put(K key, V value) {
-
// 调用上文我们已经分析过的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;
-
if ((tab = table) == null || (n = tab.length) == 0)
-
// 第一次put时,会调用resize进行桶数组初始化
-
n = (tab = resize()).length;
-
// 根据数组长度和哈希值相与来寻址,原理上文也分析过
-
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;
-
}
-
}
-
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;
-
}
相比之下Java 7中的put方法就简单不少
-
public V put(K key, V value) {
-
// 如果 key 为 null,调用 putForNullKey 方法进行处理
-
if (key == null)
-
return putForNullKey(value);
-
int hash = hash(key.hashCode());
-
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;
-
}
-
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);
-
if (size++ >= threshold)
-
resize(2 * table.length); // ②
-
}
这里有一个小细节,HashMap允许putkey为null的键值对,但是这样的键值对都放到了桶数组的第0个桶中。
3.4 resize()
resize是整个HashMap中最复杂的一个模块,如果在put数据之后超过了threshold的值,则需要扩容,扩容意味着桶数组大小变化,我们在前文中分析过,HashMap寻址是通过index =(table.length - 1) & key.hash();来计算的,现在table.length发生了变化,势必会导致部分key的位置也发生了变化,HashMap是如何设计的呢?
这里就涉及到桶数组长度为2的正整数幂的第二个优势了:当桶数组长度为2的正整数幂时,如果桶发生扩容(长度翻倍),则桶中的元素大概只有一半需要切换到新的桶中,另一半留在原先的桶中就可以,并且这个概率可以看做是均等的。
通过这个分析可以看到如果在即将扩容的那个位上key.hash()的二进制值为0,则扩容后在桶中的地址不变,否则,扩容后的最高位变为了1,新的地址也可以快速计算出来newIndex = oldCap + oldIndex;
下面是Java 8中的实现:
-
final Node<K,V>[] resize() {
-
Node<K,V>[] oldTab = table;
-
int oldCap = (oldTab == null) ? 0 : oldTab.length;
-
int oldThr = threshold;
-
int newCap, newThr = 0;
-
if (oldCap > 0) {
-
// 如果oldCap > 0则对应的是扩容而不是初始化
-
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
-
}
-
else if (oldThr > 0) // initial capacity was placed in threshold
-
// 如果oldCap为0, 但是oldThr不为0,则代表的是table还未进行过初始化
-
newCap = oldThr;
-
else { // zero initial threshold signifies using defaults
-
newCap = DEFAULT_INITIAL_CAPACITY;
-
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
-
}
-
if (newThr == 0) {
-
// 如果到这里newThr还未计算,比如初始化时,则根据容量计算出新的阈值
-
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) {
-
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;
-
// hiHead和hiTail代表元素在新的桶中和旧的桶中的位置不一致
-
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;
-
// loHead和loTail代表元素在新的桶中和旧的桶中的位置一致
-
newTab[j] = loHead;
-
}
-
if (hiTail != null) {
-
hiTail.next = null;
-
// 新的桶中的位置 = 旧的桶中的位置 + oldCap, 详细分析见前文
-
newTab[j + oldCap] = hiHead;
-
}
-
}
-
}
-
}
-
}
-
return newTab;
-
}
Java 7中的resize方法相对简单许多:
- 基本的校验之后new一个新的桶数组,大小为指定入参
- 桶内的元素根据新的桶数组长度确定新的位置,放置到新的桶数组中
-
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];
-
boolean oldAltHashing = useAltHashing;
-
useAltHashing |= sun.misc.VM.isBooted() &&
-
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
-
boolean rehash = oldAltHashing ^ useAltHashing;
-
transfer(newTable, rehash);
-
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) {
-
//链表跟table[i]断裂遍历,头部往后遍历插入到newTable中
-
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;
-
}
-
}
-
}
4. 总结
在看完了HashMap在Java 8和Java 7的实现之后我们回答一下前文中提出来的那几个问题:
- HashMap内部的bucket数组长度为什么一直都是2的整数次幂答:这样做有两个好处,第一,可以通过(table.length - 1) & key.hash()这样的位运算快速寻址,第二,在HashMap扩容的时候可以保证同一个桶中的元素均匀地散列到新的桶中,具体一点就是同一个桶中的元素在扩容后一般留在原先的桶中,一般放到了新的桶中。
- HashMap默认的bucket数组是多大答:默认是16,即时指定的大小不是2的整数次幂,HashMap也会找到一个最近的2的整数次幂来初始化桶数组。
- HashMap什么时候开辟bucket数组占用内存答:在第一次put的时候调用resize方法
- HashMap何时扩容?答:当HashMap中的元素熟练超过阈值时,阈值计算方式是capacity * loadFactor,在HashMap中loadFactor是0.75
- 桶中的元素链表何时转换为红黑树,什么时候转回链表,为什么要这么设计?答:当同一个桶中的元素数量大于等于8的时候元素中的链表转换为红黑树,反之,当桶中的元素数量小于等于6的时候又会转为链表,这样做的原因是避免红黑树和链表之间频繁转换,引起性能损耗
- Java 8中为什么要引进红黑树,是为了解决什么场景的问题?答:引入红黑树是为了避免hash性能急剧下降,引起HashMap的读写性能急剧下降的场景,正常情况下,一般是不会用到红黑树的,在一些极端场景下,假如客户端实现了一个性能拙劣的hashCode方法,可以保证HashMap的读写复杂度不会低于O(lgN)public int hashCode() {
return 1;
} - HashMap如何处理key为null的键值对?答:放置在桶数组中下标为0的桶中
HashMap线程不安全问题体现在哪
1. 多线程put导致元素丢失
1.1 源码分析
1.2 举例
2. put和get并发时,可能导致get为null
2.1 源码分析
3. 1.7多线程下扩容死循环
1. 多线程put导致元素丢失
多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
1.1 源码分析
我们来看下JDK 1.8 中 put 方法的部分源码,重点看黄色部分:
1.2 举例
假设线程1和线程2同时执行put,线程1执行put(“1”, “A”),线程2执行put(“5”, “B”),假如都冲突在table[1]这里了。注:下面的例子,只演示了 #1 和#2代码的情况,其他代码也会出现类似情况。
正常情况下,put完成后,table的状态应该是下图中的任意一个。
下面来看看异常情况:
两个线程都执行了#1处的if ((p = tab[i = (n - 1) & hash]) == null)这句代码。
此时假设线程1 先执行#2处的tab[i] = newNode(hash, key, value, null),那么table会变成如下状态:
紧接着线程2 执行tab[i] = newNode(hash, key, value, null),此时table会变成如下状态:
这样一来,元素A就丢失了。
2. put和get并发时,可能导致get为null
线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。
2.1 源码分析
我们来看下JDK 1.8 中 resize 方法的部分源码,重点看黄色部分:
在代码#1位置,用新计算的容量new了一个新的hash表,#2将新创建的空hash表赋值给实例变量table。
注意此时实例变量table是空的,如果此时另一个线程执行get,就会get出null。
3. 1.7多线程下扩容死循环
JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
来源:https://zhuanlan.zhihu.com/p/345237682?ivk_sa=1024320u
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/yzx3105/article/details/127448171