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

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

        // 遍历 HashMap
        for (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 824
 static 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;
                    }
                    //检查链表中是否已包含key
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果key存在则覆盖value
            if (e != null) { // existing mapping for key
                V 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;
        //旧的数组长度大于0
        if (oldCap > 0) {
            //旧的数组长度是否大于等于hashmap的最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                //扩容阈值就是 int的最大值2的31次方-1
                threshold = Integer.MAX_VALUE;
                //此时已经是最大值了,不扩容直接返回旧数组
                return oldTab;
            }
            //新数组长度=2*旧数组长度 小于hashmap的最大容量 且旧数组长度大于默认值16 13644391557
            else 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 threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = 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 order
                        Node<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;
                                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;
                            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.kler.cn/a/315739.html

相关文章:

  • STM32嵌入式闹钟系统设计与实现
  • 软件设计师-计算机网络
  • 【设计模式】关联关系与依赖关系
  • 大数据面试题--kafka夺命连环问(后10问)
  • HarmonyOS ArkTS 下拉列表组件
  • AI写作(四)预训练语言模型:开启 AI 写作新时代(4/10)
  • 【bug】通过lora方式微调sdxl inpainting踩坑
  • 用uniapp 及socket.io做一个简单聊天 升级 9
  • 【LeetCode】289.生命游戏
  • 模擬器怎麼多開換IP?
  • 【无人机设计与控制】 基于matlab的蚁群算法优化无人机uav巡检
  • Redis面试真题总结(一)
  • 数据库(选择题)
  • 【最快最简单的排序 —— 桶排序算法】
  • 华为HarmonyOS地图服务 11 - 如何在地图上增加点注释?
  • Java 入门基础篇08 - Java的变量与数据类型的认识
  • 在 Python 中使用 JSON
  • 【Linux取经之路】Linux项目自动化构建工具-make/makefile git三板斧
  • 基于web的工作管理系统设计与实现
  • MacOS升级Ruby版本的完整指南
  • Apache subversion 编译流程
  • Delphi 12.2 新增的 WebStencils 尝鲜
  • Vue.js与Flask/Django后端配合
  • HarmonyOS鸿蒙开发实战(5.0)表情图片聊天案例实践
  • 后端-navicat查找语句(单表与多表)
  • atcoder abc372 启发式合并, dp