当前位置: 首页 > news >正文

HashMap源码

简介

HashMap 是一种基于哈希表的 Map 接口实现,它存储键值对(key-value pairs),并允许使用键来快速检索值。在 Java 中,HashMapjava.util 包的一部分,它不是同步的,这意味着它不是线程安全的。如果你需要线程安全的版本,可以使用 ConcurrentHashMap

以下是 HashMap 的一些关键特性:

  1. 允许空键和空值HashMap 允许键和值为 null
  2. 非同步HashMap 不是线程安全的。如果你需要线程安全的 HashMap,可以使用 Collections.synchronizedMap 方法包装一个 HashMap,或者使用 ConcurrentHashMap
  3. 迭代顺序:从 JDK 1.8 开始,HashMap 保证在并发访问的情况下,迭代顺序是可预测的,即按照插入顺序进行迭代。
  4. 初始容量和加载因子HashMap 可以指定初始容量和加载因子来优化性能。初始容量是哈希表中桶的数量,加载因子是一个影响哈希表扩展的阈值。
  5. 哈希冲突:当两个键的哈希码相同,或者哈希码经过数组索引计算后位置相同时,会发生哈希冲突。HashMap 使用链表(在 JDK 1.8 之后是链表和红黑树的结合)来解决冲突。

以下是 Java 中 HashMap 的一个简单示例:

import java.util.HashMap;public class HashMapExample {public static void main(String[] args) {HashMap<String, Integer> map = new HashMap<>();map.put("apple", 1);map.put("banana", 2);map.put("cherry", 3);// 获取值Integer cherryValue = map.get("cherry");System.out.println("Cherry value: " + cherryValue);// 检查键是否存在if (map.containsKey("banana")) {System.out.println("Banana is in the map.");}// 删除键值对map.remove("apple");// 遍历 HashMapfor (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + " : " + entry.getValue());}}
}

在这个示例中,我们创建了一个 HashMap 实例,并添加了一些键值对。然后我们获取了一个值,检查了一个键是否存在,删除了一个键值对,并遍历了 HashMap

源码

hashmap成员变量

//创建 HashMap 时未指定初始容量情况下的默认容量,也就是16   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //HashMap 的最大容量,也就是2的30次方=1 073 741 824static final int MAXIMUM_CAPACITY = 1 << 30;//HashMap 默认的装载因子,当 HashMap 中元素数量超过 容量*装载因子 时,进行resize()操作,也就是扩容static final float DEFAULT_LOAD_FACTOR =	 0.75f;//用来确定何时将解决 hash 冲突的链表转变为红黑树static final int TREEIFY_THRESHOLD = 8;// 用来确定何时将解决 hash 冲突的红黑树转变为链表static final int UNTREEIFY_THRESHOLD = 6;//当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于MIN_TREEIFY_CAPACITY)导致的hash冲突太多,则不进行链表转变为红黑树操作,会先利用resize()函数对hashMap扩容static final int MIN_TREEIFY_CAPACITY = 64;// 这个就是hashMap的内部数组了,而Node则是链表节点对象。
transient Node<K,V>[] table;// 数组扩容阈值。
int threshold;

hashmap的节点的数据结构

    static class Node<K,V> implements Map.Entry<K,V> {//存储了键对象的哈希码,用于快速定位桶的位置final int hash;//存储键对象final K key;//存储与键相关联的值V value;//指向下一个节点的引用,用于解决哈希冲突,当链表长度好过一定阈值,列表会转成红黑树,以提高搜索效率Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}

hashmap的put方法

  public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}
 /*** Implements Map.put and related methods.** @param hash hash for key* @param key the key* @param value the value to put* @param onlyIfAbsent if true, don't change existing value 当存入键值对时,如果该key已存在,是否覆盖它的value。false为覆盖,true为不覆盖* @param evict if false, the table is in creation mode. 用于子类* @return previous value, or null if none*///final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab;//内部数组Node<K,V> p; //hash对应的索引中的首节点int n, i;//n 内部数组的长度  i hash对应的索引位//首次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);else {for (int binCount = 0; ; ++binCount) {//到达链表尾部,if ((e = p.next) == null) {//向链表尾部插入新节点p.next = newNode(hash, key, value, null);//判断链表元素>=8-1 ? 尝试转换红黑树 : 不转换if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//转换之前会先判断当前数组容量是否>=64 ? 转换红黑树 : resize扩容treeifyBin(tab, hash);break;}//检查链表中是否已包含keyif (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//如果key存在则覆盖valueif (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}//fail-fast机制 // Java集合中的一种错误检测机制,当多个线程对集合进行结构性的改变时,有可能会出发fail-fast机制,这个时候会抛出ConcurrentModificationException异常。++modCount;//集合实际被修改的次数//如果集合容量大于阈值,则进行扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

hashmap的resize扩容操作

   final Node<K,V>[] resize() {//旧的数组 hashmap的内部数组Node<K,V>[] oldTab = table;//旧的数组长度 如果数组长度为空=0,不为空则是数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧的扩容阈值int oldThr = threshold;int newCap, newThr = 0;//旧的数组长度大于0if (oldCap > 0) {//旧的数组长度是否大于等于hashmap的最大容量if (oldCap >= MAXIMUM_CAPACITY) {//扩容阈值就是 int的最大值2的31次方-1threshold = Integer.MAX_VALUE;//此时已经是最大值了,不扩容直接返回旧数组return oldTab;}//新数组长度=2*旧数组长度 小于hashmap的最大容量 且旧数组长度大于默认值16 13644391557else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//旧数组长度等于0 且旧的阈值大于0 新的初始数组容量被置为旧的阈值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}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"})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)//将一个红黑树节点分裂成两个子树。这个过程通常发生在并发环境下,当哈希表的大小超过某个阈值时,会将链表转换为红黑树以提高搜索效率。// 这里的 split 方法就是将一个红黑树节点分裂成两个子树,并将它们重新链接到哈希表的桶中。((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;//根据节点的哈希值与旧容量 oldCap 的按位与操作的结果将它们分配到两个新的链表中。e.hash & oldCap 的结果决定了节点应该属于哪个新链表。do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

hashmap的get方法

    public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}

get方法查找对应节点

    final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//判断容器是否为空if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//查看哈希值是否与首节点的哈希值相同if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;//首节点不是目标节点且首节点的下一个节点不为空if ((e = first.next) != null) {//红黑树结构查找节点if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//链表结构查找节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}

HashMap中的死锁

HashMap会造成死锁,因为HashMap是线程非安全的,并发的情况容易造成死锁,若要高并发推荐使用ConcurrentHashMap,这里的加了锁。

我们假设有二个进程T1、T2,HashMap容量为2,T1线程放入key A、B、C、D、E。在T1线程中A、B、C
Hash值相同,于是形成一个链,假设为A->C->B,而D、E
Hash值不同,于是容量不足,需要新建一个更大尺寸的hash表,然后把数据从老的Hash表中
迁移到新的Hash表中(refresh)。这时T2进程闯进来了,T1暂时挂起,T2进程也准备放入新的key,这时也
发现容量不足,也refresh一把。refresh之后原来的链表结构假设为C->A,之后T1进程继续执行,链接结构
为A->C,这时就形成A.next=B,B.next=A的环形链表。一旦取值进入这个环形链表就会陷入死循环。

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法向前推进。

HashMap 可能导致死锁的情况通常与以下因素有关:

重入:当一个线程在扩容(rehashing)过程中,如果另一个线程尝试插入元素,可能会触发新的扩容操作。如果多个线程同时进行这样的操作,它们可能会相互等待对方释放锁,从而导致死锁。并发修改:如果多个线程同时尝试修改 HashMap 的结构(如插入或删除元素),而这些操作没有适当的同步控制,就可能导致死锁。不当的迭代:在多线程环境中,如果一个线程在迭代 HashMap 的同时,另一个线程修改了 HashMap,可能会导致迭代器抛出 ConcurrentModificationException。虽然这通常不会导致死锁,但如果迭代器的实现不当,或者在处理异常时没有正确地同步,也可能间接导致死锁。

为了避免 HashMap 的死锁问题,可以采取以下措施:

使用 ConcurrentHashMap:这是 Java 提供的一个线程安全的 HashMap 实现。它通过分段锁(segmentation)来允许并发的读写操作,从而避免了死锁。同步包装:可以使用 Collections.synchronizedMap 方法将 HashMap 包装为线程安全的 Map。但这种方式在高并发环境下性能可能不佳,因为它对整个 Map 加锁,而不是对单个元素。显式锁:可以使用 ReentrantLock 或 synchronized 块来手动管理对 HashMap 的访问,确保在修改 HashMap 时只有一个线程能够进行操作。不可变 Map:如果不需要修改 Map,可以使用 Collections.unmodifiableMap 创建一个不可修改的视图,这样即使在多线程环境下也不会出现死锁。避免在循环中进行结构修改:在循环中进行 HashMap 的结构修改(如扩容)可能会导致死锁,应该避免这种情况。使用读写锁:如果读操作远多于写操作,可以考虑使用读写锁(ReentrantReadWriteLock),它允许多个读线程同时访问,但写线程需要独占访问。
http://www.lryc.cn/news/443944.html

相关文章:

  • 探索 Web Speech API:实现浏览器语音识别与合成
  • python基础题练习
  • 工业交换机如何保证数据的访问安全
  • jmeter得到的文档数据处理
  • 12- 【JavaWeb】校园快递管理系统-数据库建设
  • Windows本地连接远程服务器并创建新用户详细记录
  • 【kaggle竞赛】毒蘑菇的二元预测题目相关信息和思路求解代码
  • Pytest-allure如何在测试完成后自动生成完整报告?
  • 数据结构-树(基础,分类,遍历)
  • CodeGeeX4:程序员的高效助手,多语言代码生成神器!
  • 小程序组件间通信
  • Homebrew安装与切换下载源
  • C#回调函数
  • Matplotlib绘制热力图
  • 手写SpringMVC
  • mysql学习教程,从入门到精通,SQL 删除数据(DELETE 语句)(18)
  • 周边游小程序开发
  • 初级前端面试
  • 微软AI核电计划
  • 图片马赛克处理(Java)
  • python+selenium实现自动联网认证,并实现断网重连
  • 基于机器学习的注意力缺陷/多动障碍 (ADHD)(python论文+代码)HYPERAKTIV
  • Spring Boot 集成 Redisson 实现消息队列
  • go语言Map详解
  • C++——已知数组a[6]={1,3,5,7,9};输入一个数值,要求按照现有排序规律将它放入数组当中。
  • 云计算第四阶段---CLOUD Day7---Day8
  • 深入解析ThingsBoard与ThingsKit物联网平台的差异
  • 五、CAN总线
  • Linux:终端(terminal)与终端管理器(agetty)
  • 钉钉与MySQL对接集成获取部门列表2.0打通EXECUTE语句