• 深度剖析Java HashMap:源码分析、线程安全与最佳实践


    Java中的HashMap是最常用的数据结构之一,在实际开发中起着至关重要的作用。本文将详细探讨HashMap的工作原理、源码分析、线程安全问题、以及扩容机制等方面。

    一、HashMap的基本概念

    HashMap是Java集合框架中的一个类,提供了基于哈希表的数据结构。它允许存储键值对,并通过键快速检索对应的值。HashMap允许键和值为null,并且不保证映射的顺序。

    二、HashMap的工作原理

    HashMap通过哈希函数将键映射到桶(bucket)数组中的一个位置,以实现快速查找。基本操作如put和get的时间复杂度为O(1)。

    1. 哈希函数

    HashMap使用键的hashCode()方法计算哈希值,然后通过取模运算(hash % array.length)将哈希值映射到数组的索引位置。例如:

    1. int hash = key.hashCode();
    2. int index = (array.length - 1) & hash;
    2. 处理哈希冲突

    当两个不同的键有相同的哈希值时,会发生哈希冲突。HashMap使用链地址法(separate chaining)处理冲突,即每个桶存储一个链表或红黑树。当链表长度超过阈值(默认为8)时,链表转换为红黑树,以提高查询效率。

    三、源码分析

    以下是HashMap的核心代码段,包含put方法和get方法。

    1. put方法
    1. public V put(K key, V value) {
    2. return putVal(hash(key), key, value, false, true);
    3. }
    4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    5. Node[] tab; Node p; int n, i;
    6. if ((tab = table) == null || (n = tab.length) == 0)
    7. n = (tab = resize()).length;
    8. if ((p = tab[i = (n - 1) & hash]) == null)
    9. tab[i] = newNode(hash, key, value, null);
    10. else {
    11. Node e; K k;
    12. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
    13. e = p;
    14. else if (p instanceof TreeNode)
    15. e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
    16. else {
    17. for (int binCount = 0; ; ++binCount) {
    18. if ((e = p.next) == null) {
    19. p.next = newNode(hash, key, value, null);
    20. if (binCount >= TREEIFY_THRESHOLD - 1)
    21. treeifyBin(tab, hash);
    22. break;
    23. }
    24. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
    25. break;
    26. p = e;
    27. }
    28. }
    29. if (e != null) {
    30. V oldValue = e.value;
    31. if (!onlyIfAbsent || oldValue == null)
    32. e.value = value;
    33. afterNodeAccess(e);
    34. return oldValue;
    35. }
    36. }
    37. ++modCount;
    38. if (++size > threshold)
    39. resize();
    40. afterNodeInsertion(evict);
    41. return null;
    42. }
    2. get方法
    1. public V get(Object key) {
    2. Node e;
    3. return (e = getNode(hash(key), key)) == null ? null : e.value;
    4. }
    5. final Node getNode(int hash, Object key) {
    6. Node[] tab; Node first, e; int n; K k;
    7. if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
    8. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
    9. return first;
    10. if ((e = first.next) != null) {
    11. if (first instanceof TreeNode)
    12. return ((TreeNode)first).getTreeNode(hash, key);
    13. do {
    14. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
    15. return e;
    16. } while ((e = e.next) != null);
    17. }
    18. }
    19. return null;
    20. }
    3. 线程不安全的原因

    上述put和get方法在多线程环境中是不安全的。具体原因如下:

    put方法线程不安全分析
    1. 扩容(resize):当HashMap需要扩容时,可能会导致多个线程同时进行扩容操作。这会导致数据丢失和不一致。

      1. if (++size > threshold)
      2. resize();
    2. 插入节点(newNode):插入节点时,多个线程可能会同时访问同一个桶位置,导致链表或树结构损坏。

      1. if ((p = tab[i = (n - 1) & hash]) == null)
      2. tab[i] = newNode(hash, key, value, null);
    3. 链表操作:在处理哈希冲突时,链表或红黑树的插入操作不是原子的,可能会导致链表结构损坏。

      1. for (int binCount = 0; ; ++binCount) {
      2. if ((e = p.next) == null) {
      3. p.next = newNode(hash, key, value, null);
      4. if (binCount >= TREEIFY_THRESHOLD - 1)
      5. treeifyBin(tab, hash);
      6. break;
      7. }
      8. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
      9. break;
      10. p = e;
      11. }
    get方法线程不安全分析
    1. 读取不一致:在读取节点时,如果另一个线程正在进行插入或删除操作,可能会导致读取的数据不一致。

      1. if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
      2. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
      3. return first;
      4. if ((e = first.next) != null) {
      5. if (first instanceof TreeNode)
      6. return ((TreeNode)first).getTreeNode(hash, key);
      7. do {
      8. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
      9. return e;
      10. } while ((e = e.next) != null);
      11. }
      12. }

    由于这些原因,HashMap在多线程环境中使用时可能会导致不可预测的问题。因此,在多线程环境中,建议使用ConcurrentHashMap替代HashMap

    四、线程安全的解决方案
    1. 使用ConcurrentHashMap
    1. import java.util.concurrent.ConcurrentHashMap;
    2. import java.util.concurrent.ExecutorService;
    3. import java.util.concurrent.Executors;
    4. public class ConcurrentHashMapExample {
    5. private static final ConcurrentHashMap map = new ConcurrentHashMap<>();
    6. public static void main(String[] args) {
    7. ExecutorService executorService = Executors.newFixedThreadPool(10);
    8. // 使用多个线程并发访问和修改ConcurrentHashMap
    9. for (int i = 0; i < 100; i++) {
    10. final int key = i;
    11. executorService.execute(() -> map.put(key, "Value" + key));
    12. }
    13. // 读取ConcurrentHashMap中的数据
    14. executorService.execute(() -> {
    15. for (int i = 0; i < 100; i++) {
    16. System.out.println("Key: " + i + ", Value: " + map.get(i));
    17. }
    18. });
    19. executorService.shutdown();
    20. }
    21. }
    五、HashMap的初始值设置

    在实际开发中,合理设置HashMap的初始容量和负载因子可以提高性能,减少扩容次数。

    1. 初始容量

    初始容量是HashMap创建时桶数组的大小,默认值为16。初始容量应根据预期的元素数量和负载因子计算:

    int initialCapacity = (int) (expectedSize / loadFactor) + 1;
    

    例如,如果预期有100个元素,负载因子为0.75:

    int initialCapacity = (int) (100 / 0.75) + 1; // 约等于134
    2. 负载因子

    负载因子是HashMap在扩容之前允许的最大填充比例,默认值为0.75。负载因子越小,HashMap的空间利用率越低,但查找效率更高。一般情况下,使用默认负载因子即可。

    Map map = new HashMap<>(initialCapacity, 0.75f);

    合理设置初始容量和负载因子,可以避免频繁扩容,提高性能。在不确定具体情况时,默认值通常是一个好的选择。

    六、HashMap家族中的其他实现

    在Java中,除了HashMap,还有其他几个基于哈希表的数据结构实现,它们各自有不同的特点和用途。

    1. LinkedHashMap

    LinkedHashMap继承自HashMap,并且保留了插入顺序。它使用一个双向链表来维护插入顺序,可以用于需要保持元素顺序的场景。

    1. Map linkedHashMap = new LinkedHashMap<>();
    2. linkedHashMap.put(1, "one");
    3. linkedHashMap.put(2, "two");
    4. linkedHashMap.put(3, "three");
    5. System.out.println(linkedHashMap); // 输出顺序为1, 2, 3
    2. ConcurrentHashMap

    ConcurrentHashMap是一个线程安全的HashMap实现,使用了分段锁(segment locking)机制来提高并发性能。适用于高并发场景。

    1. Map concurrentHashMap = new ConcurrentHashMap<>();
    2. concurrentHashMap.put(1, "one");
    3. concurrentHashMap.put(2, "two");
    4. concurrentHashMap.put(3, "three");
    5. System.out.println(concurrentHashMap);
    3. WeakHashMap

    WeakHashMap是一种使用弱引用(weak reference)的哈希表实现。其键在没有其他强引用时可以被垃圾回收器回收。适用于缓存和内存敏感的场景。

    1. Map weakHashMap = new WeakHashMap<>();
    2. Integer key = new Integer(1);
    3. weakHashMap.put(key, "one");
    4. key = null;
    5. System.gc();
    6. System.out.println(weakHashMap); // 可能为空,因为key可能被回收
    4. IdentityHashMap

    IdentityHashMap使用键的引用相等性(reference equality)而不是键的equals方法来比较键。适用于需要比较对象引用而不是对象内容的场景。

    1. Map identityHashMap = new IdentityHashMap<>();
    2. identityHashMap.put(new Integer(1), "one");
    3. identityHashMap.put(new Integer(1), "one again");
    4. System.out.println(identityHashMap.size()); // 输出2
  • 相关阅读:
    MyBatis
    大疆笔试题——FPGA开发工程师
    [java]类
    OEKO-TEX® 推出 RESPONSIBLE BUSINESS 工具和认证
    软件测试常用的功能测试方法
    安装CUDA、anaconda、pytorch
    BottomSheetDialogFragment大量踩坑-自适应高度和最大高度和滚动问题等等
    单片机通用Bootloader框架-优化
    使用 SAP WebIDE 将 SAP UI5 应用部署到 ABAP 系统时遇到的关于传输请求的错误
    Netty 如何做到单机百万并发?
  • 原文地址:https://blog.csdn.net/zcs_978176963/article/details/139419215