JDK1.7中关于多线程操作HashMap的成环以及丢失问题

本文深入探讨了JDK1.7中HashMap在多线程环境下出现的成环及Entry丢失问题,通过具体示例和动图详细解析了这些问题的产生原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDK1.7中关于多线程操作HashMap的成环以及丢失问题

1. 预备知识

扩容产生的条件:

  • 如果HashMap初始化的时候,没有设置初始容量,那么初始容量默认为16,扩容阈值threshold为16*0.75f=12;
  • 如果设置了初始容量,那么首次扩容阈值threshold为初始容量。

HashMap的插入

  • JDK1.7的插入是头插法
	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();
}

那么在添加Entry的时候,会进行判断

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {//size>=threshold,并且需要插入的位置不为空,可能出现扩容
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

addEntry->resize->transfer

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);
}
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;
        }
    }
}

2. 关于成环

这里为了表示方便,我们设置初始容量为8,那么必须当size达到8的时候,再往里添加键值对才可能出现扩容现象。
我们使用的键值以及其在长度8和长度16的情况下对应的位置如下

Han1250--->len=8:1---- len=16:1
Han1251--->len=8:2---- len=16:2
Han1252--->len=8:3---- len=16:3
Han1253--->len=8:4---- len=16:4
Han1254--->len=8:5---- len=16:5
Han1255--->len=8:6---- len=16:6
ZH1252222--->len=8:6---- len=16:6
ZH125--->len=8:6---- len=16:6

那么在长度为8时,数据的排布情况为:
在这里插入图片描述
接下来我们默认以蓝色数组代表线程A创建的newTableA,绿色数组代表线程B创建的newTableB,新数组的长度都为原来的二倍,初始长度为8,旧数组命名为oldTble,扩容后长度为16。

2.1 线程A

在这里插入图片描述
在线程A中将前五个元素插入
在这里插入图片描述
接下来进入重点

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;//④
        }
    }
}

在这里插入图片描述
线程A运行到位置6的Entry时,e指向ZH125next指向ZH1252222,假设此时线程A失去了CPU,线程B得到了CPU。我们称之为剥夺
在这里插入图片描述

2.2 线程B

我们略过前几步。
在这里插入图片描述

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;//④

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.3 分析一下堆中的情况

OldTable作为底层数组,与HashMap对象生命周期相同,也是在堆中开辟的空间,无论多少个线程,都是操作的这一个对象,如果不加锁进行限制,则会产生数据的错乱。
在经过了上面的操作之后,堆中的数据情况如下:

线程A和B中的enext的指向如下图:

2.4 我们重新转入线程A

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
为社么会产生这种情况呢?由于链表的插入为头插法,我们设一个链表上的两个Entry为E1E2,假设我们在线程A中,e和next分别是E1E2的引用,这时候线程B抢夺到了CPU,E1E2在新的Table数组中的位置依然相同,仍旧在同一链表中,在oldTableA中两个Entry的顺序为E1->E2,经过线程B的操作,顺序变成了E2->E1

这时候我们回到线程A中

e:E1
nex:E2

将e设置到newTableA的i处,newTableA[i]=e; e.next=newTableA[i];
接下来,e=next;,e指向了E2,while进入了下一轮循环。
进入新的循环,next=e.next;,此时e指向E2,E2.next==e.next==E1

在这里插入图片描述在这里插入图片描述
我们继续将e头插入新的链表,e=next;next=e.next;
在这里插入图片描述
在这里插入图片描述

此时我们发现,如果将e继续头插入新链表,ZH125.next==ZH1252222,而ZH1252222.next=ZH125
在这里插入图片描述

2.5 动图

在这里插入图片描述

3. 分析Entry丢失的情况

3.1 前提信息

在这里插入图片描述

3.2 线程B

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3.3 线程A

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3.4 线程B

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.5 动图

在这里插入图片描述

<think>好的,我现在需要解决JavaHashMap因头插法导致死循问题。首先,我得回忆一下HashMap的工作原理。在Java 8之前,HashMap在处理哈希冲突时使用的是链表,当多个键值对映射到同一个桶时,会以链表形式存储。当链表过长时,会进行扩容,也就是重新调整大小,这个过程叫做rehashing。 用户提到的问题是关于头插法导致死循的情况。头插法意味着在插入新节点时,会将新节点放在链表的头部而不是尾部。在扩容时,旧链表中的元素会被重新计算哈希值,然后放入新的桶中。这个时候,如果多线程同时进行扩容操作,可能会导致链表形成环形结构,进而导致死循。这种情况通常发生在多线程境下,比如多个线程同时执行put操作并触发扩容。 我记得在Java 8中,HashMap的实现已经改为了尾插法,也就是在插入链表时,新节点被放在链表的末尾。这样可以避免在扩容时出现形链表的情况,从而解决了多线程下的死循问题。因此,一个直接的解决方案是升级到Java 8或更高版本,利用新的HashMap实现。 但是,如果用户因为某些原因必须使用旧版本的Java,比如Java 7或更早,这时候该怎么办呢?可能需要考虑其他的解决方案。比如,使用线程安全的ConcurrentHashMap来代替HashMap。ConcurrentHashMap内部使用了分段锁等机制来保证线程安全,避免了多线程操作时的竞争问题。 另外,用户可能需要意识到,即使使用了尾插法,HashMap本身也不是线程安全的。在多线程境下,即使没有死循问题,仍然可能出现数据不一致或其他并发问题。因此,正确的做法是使用线程安全的集合类,如ConcurrentHashMap,或者通过外部同步机制来保证线程安全。 再查一下相关的资料,比如引用中的内容,虽然引用中的代码是关于Java的IO操作和异常处理的,但可能没有直接相关的信息。不过用户提供的引用可能是在其他上下文中提到的,需要确认是否有相关的解决方案。例如,引用[1]中的flush操作可能与资源管理有关,但和HashMap问题关系不大。引用[3]提到了objc_setAssociatedObject,这是Objective-C的API,可能与当前问题无关。 因此,综合来看,主要的解决方案是升级到Java 8+,或者使用ConcurrentHashMap,或者确保在单线程境下使用HashMap,避免多线程并发修改。同时,用户需要了解HashMap的线程不安全性,并在多线程境下采取适当的同步措施。 最后,生成相关问题时要围绕HashMap的实现、多线程问题、解决方案等展开,帮助用户进一步理解相关知识点。</think>### Java HashMap头插法导致死循的解决方案 在Java 8之前的版本中,`HashMap`在多线程扩容时可能因头插法导致链表成环,从而引发死循问题。以下是具体原因和解决方案: #### **问题原因** 1. **头插法实现** Java 7及更早版本的`HashMap`在插入链表节点时,将新节点置于链表头部(例如`entry.next = table[bucketIndex];`)。扩容时,旧链表的节点会逆序迁移到新数组的桶中。 2. **多线程竞争** 若两个线程同时触发扩容(`resize()`),且旧链表的迁移顺序为`A -> B -> C`,线程1可能在迁移过程中被挂起,而线程2完成迁移后链表变为`C -> B -> A`。当线程1恢复执行时,可能因引用混乱形成环形链表(如`B.next = A`且`A.next = B`),导致后续操作陷入死循[^1]。 #### **解决方案** 1. **升级到Java 8+** Java 8将链表插入方式改为尾插法,迁移时保持链表顺序不变,避免形链表的产生。例如: ```java // Java 8 HashMap.resize() 代码片段(简化) 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); ``` 通过维护头尾指针,新链表顺序与旧链表一致[^2]。 2. **使用线程安全的容器** 在多线程场景中,优先使用`ConcurrentHashMap`。其内部通过分段锁(Java 7)或CAS+synchronized(Java 8+)保证线程安全,避免死循问题: ```java Map<String, String> safeMap = new ConcurrentHashMap<>(); ``` 3. **避免多线程直接操作HashMap** 若必须使用旧版本Java,需通过同步机制控制并发访问: ```java Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); ``` #### **代码对比示例** - **Java 7的头插法(危险)** ```java void transfer(Entry[] newTable) { for (Entry<K,V> e : table) { while (e != null) { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; // 头插法 newTable[i] = e; e = next; } } } ``` - **Java 8的尾插法(安全)** ```java final Node<K,V>[] resize() { // ... 维护loHead/loTail和hiHead/hiTail if (loTail != null) { loTail.next = null; newTab[j] = loHead; // 直接挂载链表头 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } ``` #### **总结** - **根本原因**:头插法 + 多线程竞争导致链表成环。 - **推荐方案**:升级至Java 8+,或使用`ConcurrentHashMap`。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值