HashMap/ConcurrentHashMap/头插法/尾插法

1.1 HashMap

JDK1.7

JDK1.8

存储

数组+链表

数组+链表+红黑树

位置算法

h & (length-1)

h & (length-1)

链表超过8

链表

红黑对(链表超过8且数组长度超64)

节点结构

Entry<K,V> implements Map.Entry<K,V>

Node<K,V> implements Map.Entry<K,V>

插法

头插法(扩容环化造成死循环)

尾插法

JDK1.7

使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表;这样数据遍历时间就过长。1.7中hashmap链表插入的方式是使用头插法

JDK1.8

使用一个Node数组来存储数据,但是这个Node可能是链表结构,也可能是红黑树结构;如果插入的元素key的hashcode值相同,那么这些key也会被定位到Node数组的同一个格子里,如果不超过8个使用链表存储,超过8个且Node数组长度超过64,会将链表转换为红黑树。1.8中hashmap链表插入的方式是使用尾插法

【相关问题】

问题一:为什么jdk1.8后改为尾插法?

主要是因为头插法在多线程扩容情况下会引起链表环。那什么是链表环呢?

引用一张图片(来源hashmap的头插法和尾插法_HashMap 链表插入方式 → 头插为何改成尾插 ?_邹振东的博客-优快云博客):

1.2 ConcurrentHashMap

主要是支持安全的多线程的读写

JDK1.7

JDK1.8

实现

segment+hashentry

Node+CAS+Synchronized

Segment继承ReetrantLock

Synchronized

存储

数组+链表

数组+链表+红黑树

链表超过8

数组

红黑树

插法

头插法

尾插法

 

<think>好的,我现在要解决的问题是,如何将Java HashMap头插法修改为尾插法。用户提到了JDK1.8已经做了这样的改动,但可能他想了解具体的实现方式或者自己尝试修改。首先,我需要回顾HashMap在JDK1.7和1.8中的不同实现,尤其是链表插入方式的变化。 根据提供的引用,JDK1.7使用头插法,而1.8改为了尾插法,主要是为了解决并发环境下的死循环问题。用户现在想了解如何手动修改头插法尾插法,可能是在旧版本的代码中进行调整,或者理解其实现机制。 首先,我需要明确头插法尾插法的区别。头插法是将新节点插入链表的头部,而尾插法则是插入尾部。在JDK1.7中,当发生哈希冲突时,新元素会被放在链表的头部,这可能导致在并发扩容时形成环形链表,进而导致死循环。而尾插法则避免了这个问题,同时可能提高查询效率,因为保持了插入顺序。 接下来,我需要查找HashMap源码中关于插入链表的部分。在JDK1.7中,添加元素的方法在addEntry和createEntry中,例如: void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); // ... } 这里,新Entry的构造函数将下一个节点指向原来的e,即链表头部插入。如果要改为尾插法,需要遍历到链表尾部,然后将新节点添加到末尾。 但这样每次插入都需要遍历链表,时间复杂度从O(1)变为O(n),显然会影响性能。然而,JDK1.8中使用了红黑树优化,当链表长度超过阈值时转换为红黑树,这样即使使用尾插法,也不会显著降低性能。但用户的问题可能集中在链表部分,不考虑红黑树的情况。 因此,修改头插法尾插法的关键在于,找到插入位置为链表尾部。这需要修改插入逻辑,例如在JDK1.7的代码中,遍历链表到最后一个节点,然后将新节点添加到末尾。 但这样做的问题在于每次插入都需要遍历整个链表,这在频繁插入的场景下性能会很差。所以,可能需要在数据结构上做调整,比如维护一个尾指针,但这会增加内存开销。或者,像JDK1.8那样,在插入时记录尾节点。 另外,需要考虑扩容时的处理。在JDK1.7中,扩容时也会使用头插法转移节点,这同样需要修改为尾插法,否则扩容后的链表顺序仍然可能倒置,导致并发问题。 总结步骤可能包括: 1. 修改插入方法,将新节点添加到链表尾部。 2. 处理扩容时的节点转移,同样采用尾插法。 3. 考虑维护尾指针以优化插入效率。 4. 测试修改后的实现,确保功能正确且性能可接受。 但实际中,直接修改JDK1.7的HashMap可能并不现实,因为涉及到多处代码改动,且性能可能下降。更好的做法是升级到JDK1.8及以上版本,直接使用优化后的HashMap。但用户的问题可能更多是出于学习目的,想了解实现原理。 在思考过程中,还需要验证引用中的信息是否正确,例如JDK1.8是否真的使用尾插法,以及具体实现细节。查阅源码发现,JDK1.8的HashMap在putVal方法中,当发生哈希冲突时,会遍历链表,如果未找到相同key,则将新节点添加到链表末尾。具体代码片段: if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // ... } 这里的p是当前节点,p.next为null时,说明到了链表尾部,新节点作为p的next插入,即尾插法。 因此,在JDK1.8中,尾插法的实现是通过遍历到链表尾部,然后插入新节点。而在JDK1.7中,头插法则是直接将新节点作为头节点,替换原有链表头部。 如果要在旧版本中模拟尾插法,需要修改插入逻辑,每次找到链表尾部再插入。这需要修改对应的Entry类和插入方法,同时处理扩容时的节点转移逻辑,确保在扩容时也保持尾插法。 不过,这样的修改会显著影响插入性能,特别是在长链表中。因此,JDK1.8的改进不仅仅是尾插法,还引入了红黑树来优化长链表的查询效率,从而平衡了插入和查询的性能。 最后,用户的问题可能还涉及到并发环境下的安全性,但HashMap本身不是线程安全的,ConcurrentHashMap才是。因此,在修改头插法尾插法时,如果目标是解决并发问题,可能需要结合其他机制,或者直接使用ConcurrentHashMap。</think>#### JDK 1.7 头插法修改为尾插法的实现原理 在 JDK 1.7 的 `HashMap` 中,链表插入采用**头插法**,即新节点直接插入链表头部。若需改为**尾插法**,需修改以下核心逻辑: 1. **链表插入逻辑调整** - **原头插法代码片段**(JDK 1.7): ```java void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); // 新节点成为链表头 // ... } ``` - **修改为尾插法**: 遍历链表至尾部,将新节点插入末尾: ```java void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; if (e == null) { table[bucketIndex] = new Entry<>(hash, key, value, null); } else { Entry<K,V> tail = e; while (tail.next != null) { tail = tail.next; // 遍历至链表尾部 } tail.next = new Entry<>(hash, key, value, null); // 插入尾部 } // ... } ``` 2. **扩容时节点转移逻辑调整** JDK 1.7 扩容时通过 `transfer` 方法重新计算索引并转移节点,原实现仍使用头插法: ```java void transfer(Entry[] newTable) { for (Entry<K,V> e : table) { while (e != null) { Entry<K,V> next = e.next; int newIndex = e.hash & (newCapacity - 1); e.next = newTable[newIndex]; // 头插法 newTable[newIndex] = e; e = next; } } } ``` - **修改为尾插法**: 需要维护每个链表的尾指针,避免遍历: ```java void transfer(Entry[] newTable) { Entry<K,V>[] newHeads = new Entry[newTable.length]; Entry<K,V>[] newTails = new Entry[newTable.length]; for (Entry<K,V> e : table) { while (e != null) { Entry<K,V> next = e.next; int newIndex = e.hash & (newCapacity - 1); e.next = null; if (newHeads[newIndex] == null) { newHeads[newIndex] = e; newTails[newIndex] = e; } else { newTails[newIndex].next = e; // 尾插法 newTails[newIndex] = e; } e = next; } } table = newHeads; } ``` #### 性能对比与注意事项 | 方法 | 时间复杂度 | 并发安全性 | 适用场景 | |----------|------------|---------------------|------------------------| | **头插法** | $O(1)$ | 多线程下可能形成环形链表 | 单线程环境 | | **尾插法** | $O(n)$ | 避免环形链表问题 | 需手动维护尾指针或遍历 | - **缺点**:尾插法需遍历链表,时间复杂度从 $O(1)$ 变为 $O(n)$,性能显著下降[^1]。 - **优化方向**:参考 JDK 1.8 的实现,引入**红黑树**结构(链表长度 >8 时转树),平衡插入与查询效率[^1][^4]。 #### 完整代码示例(模拟 JDK 1.7 尾插法改造) ```java public class CustomHashMap<K, V> { static class Entry<K, V> { final K key; V value; Entry<K, V> next; final int hash; Entry(int hash, K key, V value, Entry<K, V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } } private Entry<K, V>[] table; private int capacity = 16; public CustomHashMap() { table = new Entry[capacity]; } public void put(K key, V value) { int hash = key.hashCode(); int index = hash & (capacity - 1); // 尾插法实现 Entry<K, V> newEntry = new Entry<>(hash, key, value, null); if (table[index] == null) { table[index] = newEntry; } else { Entry<K, V> tail = table[index]; while (tail.next != null) { tail = tail.next; } tail.next = newEntry; } } // 其他方法省略... } ``` #### 总结 JDK 1.8 的尾插法改进通过以下方式优化性能: 1. **红黑树转换**:链表长度超过阈值时转为树结构,保证 $O(\log n)$ 的查询效率[^1][^4]。 2. **哈希算法优化**:采用 `MurmurHash` 减少哈希冲突[^1]。 --相关问题-- 1. JDK 1.8 的 `HashMap` 如何通过红黑树优化链表性能? 2. 头插法为何在并发场景下会导致死循环? 3. `ConcurrentHashMap` 如何解决线程安全问题?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栈江湖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值