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

HashMap底层原理详解:扩容、红黑树与ConcurrentHashMap的线程安全

HashMap作为Java集合框架中最重要且最常用的数据结构之一,其底层实现原理和线程安全方案是Java开发者必须掌握的核心知识。本文将深入剖析HashMap的实现机制,并详细解释ConcurrentHashMap如何保证线程安全。

一、HashMap底层数据结构

1.1 数组+链表+红黑树结构

HashMap在JDK 1.8后的底层实现采用"数组+链表+红黑树"的混合结构:

java

// HashMap中的核心数组定义
transient Node<K,V>[] table;// 链表节点定义
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;// ...
}// 红黑树节点定义
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // 红黑树链接TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // 删除后需要取消链接boolean red;// ...
}

1.2 哈希计算与索引定位

HashMap通过哈希函数确定键值对的存储位置:

java

// 计算key的哈希值
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}// 计算数组下标
int index = (table.length - 1) & hash(key);

高16位与低16位异或运算的目的是为了增加哈希值的随机性,减少哈希冲突。

二、HashMap扩容机制

2.1 扩容触发条件

HashMap在以下情况下会触发扩容:

  1. 初始化后首次插入元素:默认创建长度为16的数组

  2. 元素数量超过阈值:阈值 = 容量 × 负载因子(默认0.75)

  3. 链表长度达到8但数组长度小于64:优先扩容而不是树化

2.2 扩容过程

扩容过程主要分为以下步骤:

java

final Node<K,V>[] resize() {// 1. 计算新容量和新阈值int newCap = oldCap << 1;  // 双倍扩容float newThr = oldThr << 1;// 2. 创建新数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 3. 迁移元素(重新哈希)for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {// 处理每个桶的元素迁移if (e.next == null) {// 单个节点直接迁移newTab[e.hash & (newCap - 1)] = e;} else if (e instanceof TreeNode) {// 红黑树迁移((TreeNode<K,V>)e).split(this, newTab, j, oldCap);} else {// 链表迁移(使用高低位链表优化)Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;// ...}}}return newTab;
}

2.3 扩容优化:高低位链表

JDK 1.8对链表迁移进行了优化,通过判断(e.hash & oldCap) == 0将原链表拆分为低位链表和高位链表:

  • 低位链表:保持原索引位置不变

  • 高位链表:新索引 = 原索引 + 原容量

这样避免了重新计算哈希值,提高了扩容效率。

三、红黑树转化条件

3.1 链表转红黑树

当同时满足以下两个条件时,链表会转化为红黑树:

  1. 链表长度达到阈值8

  2. 数组长度达到最小树化容量64(否则优先扩容)

java

// 树化阈值
static final int TREEIFY_THRESHOLD = 8;// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 如果数组长度小于64,优先扩容而不是树化if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();else if ((e = tab[index = (n - 1) & hash]) != null) {// 执行树化操作TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}
}

3.2 红黑树退化为链表

当同时满足以下两个条件时,红黑树会退化为链表:

  1. 树节点数量小于等于退化阈值6

  2. 在扩容 resize 或删除 remove 操作时触发检查

java

// 退化阈值
static final int UNTREEIFY_THRESHOLD = 6;// 在树节点删除后检查是否需要退化
if (root == null || root.right == null ||(rl = root.left) == null || rl.left == null) {tab[index] = first.untreeify(map);  // 退化return;
}

四、ConcurrentHashMap线程安全实现

4.1 JDK 1.7分段锁机制

在JDK 1.7中,ConcurrentHashMap使用分段锁(Segment)实现线程安全:

java

// 分段锁结构
final Segment<K,V>[] segments;static final class Segment<K,V> extends ReentrantLock implements Serializable {// 每个Segment独立管理一个HashEntry数组transient volatile HashEntry<K,V>[] table;
}// 操作时只需要锁住对应的Segment
public V put(K key, V value) {Segment<K,V> s;// 只锁定当前Segment,不影响其他Segment的操作int hash = hash(key);int j = (hash >>> segmentShift) & segmentMask;s = ensureSegment(j);return s.put(key, hash, value, false);
}

4.2 JDK 1.8 CAS+synchronized优化

JDK 1.8放弃了分段锁,采用更细粒度的锁机制:

java

// 使用CAS和synchronized保证线程安全
final V putVal(K key, V value, boolean onlyIfAbsent) {// ...for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();  // 使用CAS初始化else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 桶为空,使用CAS添加新节点if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break;}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);  // 协助扩容else {V oldVal = null;synchronized (f) {  // 锁住桶的头节点// 执行链表或树操作if (tabAt(tab, i) == f) {if (fh >= 0) {// 链表操作} else if (f instanceof TreeBin) {// 红黑树操作}}}// ...}}
}

4.3 关键线程安全技术

  1. CAS操作:用于无竞争情况下的快速操作

    • 数组初始化

    • 空桶节点插入

    • 计数器的更新

  2. synchronized锁:用于锁定单个桶(链表头节点或树根节点)

    • 粒度更细,并发度更高

    • 与CAS配合实现高效并发

  3. volatile变量:保证内存可见性

    • table数组引用

    • 节点next指针

    • sizeCtl等控制变量

  4. 多线程协同扩容

    • 当前线程插入时发现正在扩容,会协助迁移数据

    • 通过ForwardingNode节点标识正在迁移的桶

五、HashMap与ConcurrentHashMap对比

特性HashMapConcurrentHashMap
线程安全
锁粒度无锁(非线程安全)桶级别锁(JDK1.8)
空键值允许一个null键和多个null值不允许null键或值
迭代器Fail-FastWeakly Consistent
性能单线程最优高并发下性能优异

六、实践建议

  1. 合理设置初始容量和负载因子:避免频繁扩容

    java

    // 预估元素数量,避免重复扩容
    Map<String, Object> map = new HashMap<>(expectedSize, 0.75f);
  2. 键对象实现规范哈希方法

    java

    // 重写hashCode和equals方法
    public class Key {@Overridepublic int hashCode() {// 保证哈希分布均匀}@Overridepublic boolean equals(Object obj) {// 保证一致性}
    }
  3. 高并发场景使用ConcurrentHashMap:替代Hashtable和Collections.synchronizedMap

  4. 关注树化退化阈值:理解8和6这两个关键数字的意义

总结

HashMap通过数组+链表+红黑树的混合结构实现了高效的查找和插入操作,其扩容机制和树化策略在时间和空间上达到了良好平衡。ConcurrentHashMap则通过CAS+synchronized的细粒度锁机制,在保证线程安全的同时提供了优异的并发性能。理解这些底层原理不仅有助于正确使用这些数据结构,也能为设计和优化高性能Java应用奠定坚实基础。

对于Java开发者而言,深入理解HashMap和ConcurrentHashMap的底层实现是提升技术深度的必经之路,也是应对高级别技术面试的关键准备。

http://www.dtcms.com/a/392968.html

相关文章:

  • autodl文件存储,文件同步,conda环境同步问题
  • 【ROS2】Begginer : CLI tools - 理解 ROS 2 话题
  • Java网络编程:从基础到实战
  • 面试MYSQL的索引类型、索引的工作原理、以及索引优化策略
  • 一、Pytorch安装教程-windows环境,利用Anaconda搭建虚拟环境,Pycharm开发工具
  • JWT登录校验
  • 对症下药:电商、B2B、本地服务和内容媒体的GEO定制化策略
  • 分类预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络多特征分类预测
  • pcl封装11 (快速定义)旋转矩阵
  • Windows 系统中如何通过 Docker 调用 CUDA 和 cuDNN 加速大模型推理
  • 从零编写vue3系统--5步教学
  • 嵌入式Linux C语言程序设计三
  • 【记录】初赛复习 Day5 6(2021S第一轮错题,内附深井题目讲解)
  • 【C++】类和对象—(下) 收官之战
  • 人工智能学习:什么是迁移学习
  • 模型进阶与神经网络
  • 微软.NET离线运行库合集 v2025.09.09_Win中文_NET运行库_安装教程
  • Galileo AI-AI驱动的UI界面设计工具
  • 布谷鸟布隆过滤器和计数式布隆过滤器和原始布隆过滤器相比分别解决了什么问题?
  • 大模型介绍
  • 基于Springboot的无人之境智能酒店服务平台
  • ICCV-2025 | 大模型驱动的认知导航框架!CogNav:面向目标导航的大型语言模型驱动的认知过程建模
  • java-异常
  • 网络编程:一个 TCP 服务器的简易实现(epoll 版本)
  • 【MySQL学习】关于MySql语句执行、查询、更新流程原理总结
  • C++语法深度剖析与面试核心详解
  • 【Tomcat】基础总结:类加载机制
  • 127、【OS】【Nuttx】【周边】效果呈现方案解析:比较浮点数(上)
  • 计网协议簇具体协议
  • 电路分析基础笔记