【ConcurrentHashMap】实现原理和HashMap、Redis哈希的区别

【ConcurrentHashMap】实现原理和HashMap、Redis哈希的区别

  • 【一】核心思想
    • 【1】HashMap​
      • (1)概括
      • (2)🚀线程不安全的场景和原因
        • 1-场景一:Put 操作导致的数据覆盖/丢失 (Lost Update)​​
        • 2-场景二:扩容 (Resize) 导致死循环或数据丢失 (JDK 7 及之前版本的致命问题)​​
        • 3-场景三:非原子性的 size 操作和可见性问题​
        • 4-核心结论​
        • 5-解决方案​:
    • 【2】ConcurrentHashMap (CHM)​​
  • 【二】实现原理深度解析
    • 【1】基础结构:Node 数组 + 链表 + 红黑树​
    • 【2】并发控制的三大核心机制​
      • ​(1)CAS (Compare-And-Swap) 操作 - 无锁乐观锁​
      • (2)Synchronized 锁 - 细粒度悲观锁​
      • (3)volatile 变量 - 内存可见性保证​
    • 【3】关键操作原理​
      • (1)put / putVal 操作​:CAS和synchronized
      • (2)get 操作​:无锁
      • (3)size() 操作​:CounterCell分段计数
      • (4)🔥扩容 (Transfer) 操作
        • 1-概述
        • 2-扩容的触发时机​
        • 3-扩容的核心思路​
    • 【4】扩容的详细过程
      • (1)步骤 1:初始化与规划 (单线程完成)​​
      • (2)步骤 2:多线程协助迁移 (并发完成)​​
        • 1-​场景一:其他线程也来执行 put或 remove​
        • 2-​场景二:协助过程​
      • (3)步骤 3:收尾与切换 (单线程完成)​​
      • (4)核心机制与设计哲学​
      • (5)总结
  • 【三】ConcurrentHashMap 与 HashMap 的全面区别​
  • 【四】总结与使用建议
  • 【五】补充扩展:redis的Hash如何实现扩容
    • 【1】核心数据结构​
      • (1)dict结构体​:代表整个字典。
      • (2)dictht结构体​(哈希表):
      • (3)dictEntry结构体​
    • 【2】扩容的触发时机​
      • (1)负载因子触发 (主要条件)​​
      • (2)无法完成命令触发 (强制条件)​​:
    • 【3】扩容过程:渐进式 Rehash (核心)​​
      • (1)​步骤 1:空间分配​
      • (2)​步骤 2:开始 Rehash​
      • (3)​步骤 3:逐步迁移 (最精妙的部分)​​
      • (4)​步骤 4:定时任务辅助​
      • (5)​步骤 5:完成与切换​
    • 【4】在此期间的操作如何执行?​​
      • (1)​查找 (GET, HGET)​​:
      • (2)​添加 (HSET, HMSET)​​:
      • (3)删除 (HDEL)​​:
    • 【5】总结与优势

【一】核心思想

【1】HashMap​

(1)概括

(1)​定位​:高性能的单线程键值对存储结构。
(2)​核心问题​:在多线程环境下,进行 put操作可能触发 rehash,导致链表形成环,从而引起死循环和CPU 100%​。即使不做结构性修改(只是读写),由于没有内存同步屏障,也可能导致线程间可见性问题,读取到过期的数据。
(2)​线程安全方案​:如果需要在线程中使用,必须通过外部同步(如 Collections.synchronizedMap())来包装它,但这会带来巨大的性能开销,因为锁的粒度是整个 map,导致所有操作串行化。

(2)🚀线程不安全的场景和原因

HashMap的线程不安全并非指它在多线程下会立即崩溃,而是指在多线程环境下对其进行操作会导致未定义的行为。这些行为包括数据丢失、数据不一致,甚至可能使程序卡死。其根本原因在于内部机制缺乏必要的同步保障,导致多个线程可以同时修改其内部结构。

以下是几个最经典、最致命的线程不安全场景:

1-场景一:Put 操作导致的数据覆盖/丢失 (Lost Update)​​

这是最常见且最容易理解的问题。
(1)发生原因​:
1.HashMap的 put()方法最终会调用 putVal()。
2.在插入新元素时,有一个关键步骤:​判断目标桶(bucket)是否为空。
3.如果桶为空,则通过

if ((p = tab[i = (n - 1) & hash]) == null)

判断后,直接执行 tab[i] = newNode(hash, key, value, null);将新节点放入桶中。

(2)并发执行过程​:
1.​线程 A​ 和 ​线程 B​ 同时向 HashMap 插入一个 key。
2.假设这两个 key 经过哈希计算后,落在了同一个空桶里。
3.线程 A 和线程 B 都执行了 if判断,发现 tab[i] == null为 true。
4.线程 A 先执行 tab[i] = newNode(…),成功将它的键值对放入。
5.紧接着,线程 B 也执行 tab[i] = newNode(…),​覆盖了线程 A 刚刚放入的节点。

(3)最终结果​:线程 A 存入的数据完全丢失了,仿佛从未存在过。整个 Map 中只存在线程 B 的数据。
​根本原因​:检查-然后-行动 (check-then-act)这个复合操作不是原子的,中间可能被其他线程打断。

2-场景二:扩容 (Resize) 导致死循环或数据丢失 (JDK 7 及之前版本的致命问题)​​

这是 HashMap线程不安全最著名的问题,在 JDK 7 及之前版本中会导致 ​CPU 100%​。虽然 JDK 8 改进了扩容算法,避免了死循环,但仍然可能导致数据丢失。

​JDK 7 的死循环问题 (经典头插法反转)​​

JDK 7 在扩容转移链表节点时,使用的是头插法,即新节点会插在链表的头部。这在多线程并发扩容时是灾难性的。

假设原链表在桶中的顺序是:A -> B -> null

扩容时,两个线程同时触发 resize()。
(1)线程一开始执行转移,刚执行完 A节点后被挂起。
此时在线程一的视角,新链表是 A -> null。
(2)线程二​ 获得了 CPU 时间片,完整地执行了整个链表的转移。由于是头插法,转移后的新链表顺序被反转:
B -> A -> null
(3)此时,​线程一​ 恢复执行。它仍然持有旧的 A.next引用(指向 B),它继续将 A节点用头插法插入新数组的桶中。
1-它先将 A插入,此时新桶是 A -> null(因为线程一不知道线程二已经插入了 B->A)。
2-接着,它要处理 next = e.next,即 B。它将 B插入桶的头部,即 B -> A -> null。
3-​但是!​​ 在线程二转移后的世界里,A.next本来是指向 null的,但现在线程一操作后,B.next指向了 A,而 A.next又指向了 null,这看起来是正常的。
(4)最关键的一步​:如果此时又有另一个元素 C,其哈希值也和 A、B一样,在线程二完成扩容后,链表是 B->A->null。此时如果线程一介入,并按照上述流程操作,​极有可能在特定的时序下,形成 A.next -> B且 B.next -> A的环形链表。
(5)最终结果​:当下一次对这个桶进行 get()或 put()操作时,需要遍历这个链表,就会陷入无限的死循环,导致 CPU 使用率飙升到 100%。

(6)JDK 8 的改进​:将头插法改为了尾插法。即在扩容时,保持链表节点的原有顺序。这从根源上避免了环形链表的产生。​但是,这并不代表 JDK 8 的 HashMap 就是线程安全的了!​​ 它只是避免了最致命的死循环,但并发扩容仍然会导致数据丢失等其它问题。

JDK 8+ 的数据丢失问题​

即使在 JDK 8+,多线程同时触发 resize(),虽然不会死循环,但可能会因为线程之间覆盖彼此转移的节点,或者因为 table数组的可见性问题,导致某个线程转移的数据被另一个线程覆盖,最终造成部分数据丢失。

3-场景三:非原子性的 size 操作和可见性问题​

(1)size 不准确​:HashMap的 size属性由一个普通的 int变量维护。多线程同时进行 put和 remove操作时,size的更新 (size++或 size–) 不是原子的,可能会丢失更新。导致你调用 size()方法得到的值是一个错误的值。
(2)​可见性问题 (Dirty Read)​​:由于 HashMap的内部状态(如 table数组引用、节点的 value)没有用 volatile修饰,也缺乏锁带来的内存同步效应。因此,一个线程的修改(例如成功 put了一个值)​可能不会立即被其他线程看到。导致一个线程 get()可能拿到一个过期的、旧的值,或者甚至 null。

4-核心结论​

HashMap的所有这些线程不安全行为,都源于其设计初衷是追求单线程下的极致性能,因此它刻意省略了所有昂贵的同步开销​(如 synchronized、volatile)。这些同步保障需要由使用者来负责。

在这里插入图片描述

5-解决方案​:

(1)替代方案​:在多线程环境下,使用 ​ConcurrentHashMap。它通过 CAS、synchronized(锁单个桶)、volatile等机制,在保证线程安全的同时,提供了极高的并发性能。
(2)加锁方案​:使用 Collections.synchronizedMap(new HashMap<>())来包装一个同步的 Map。但这会使用一个全局锁,性能很差,不推荐在高并发场景使用。
(3)只读方案​:如果 Map 初始化后就不会再修改,那么多线程同时读取是安全的。

【2】ConcurrentHashMap (CHM)​​

(1)定位​:高吞吐量的线程安全哈希表,专为多线程并发访问而设计。
(2)设计目标​:在保证线程安全的前提下,最大限度地提高并发性能。
(3)核心思想​:​锁分段技术 (JDK 7) -> 锁粒度细化 (JDK 8)​。它不像 Hashtable或同步的 HashMap那样用一把大锁锁住整个表,而是采用更细粒度的锁机制,使得多个读操作和一定条件下的写操作可以并发进行。

【二】实现原理深度解析

JDK 8 对 ConcurrentHashMap进行了彻底的重写,摒弃了之前的分段锁 (Segment)​​ 概念,采用了更优化的实现方式。

【1】基础结构:Node 数组 + 链表 + 红黑树​

与 HashMap类似,CHM 的基础结构也是一个 Node<K,V>[] table数组。每个 Node是一个链表节点或树节点(TreeNode)。
(1)当链表长度超过一定阈值(默认为 8)且数组长度达到一定规模(默认为 64)时,链表会转换为红黑树,以优化极端情况下的查询性能(从 O(n) 提升到 O(log n))。
(2)当树节点变得足够小时,会退化为链表。

【2】并发控制的三大核心机制​

​(1)CAS (Compare-And-Swap) 操作 - 无锁乐观锁​

CAS 是一种乐观的原子操作,用于实现无锁(lock-free)的非阻塞算法。在 CHM 中,CAS 被广泛应用于非竞争性的场景,避免了昂贵的锁开销。

​应用场景​:
(1)初始化 table 数组。
(2)将新节点插入到空桶(bucket)中。如果发现目标桶是空的,则直接用 CAS 操作将新节点设置进去,而不是先获取锁。
(3)协助扩容时设置转移标记。

(2)Synchronized 锁 - 细粒度悲观锁​

当 CAS 无法完成操作时(例如,要向非空桶的链表或树中插入节点),会退化成使用传统的 synchronized锁。
(1)​锁的粒度​:​锁的不是整个 map,也不是某个段,而是单个桶(即数组的第一个节点)​。这是 JDK 8 实现相比 JDK 7 分段锁的一大优势,粒度更细,并发度更高。
(2)​工作流程​:
1.计算 key 的哈希值,定位到具体的桶。
2.如果桶为空,使用 CAS 插入。
3.如果桶不为空,则 synchronized锁定这个桶的头节点。
4.在锁内进行链表或树的操作(插入、更新、删除)。
5.操作完成后释放锁。
由于锁的粒度非常小,只是众多桶中的一个,因此绝大多数情况下,线程可以并发地访问不同的桶,极大提升了吞吐量。

(3)volatile 变量 - 内存可见性保证​

所有重要的属性(如 table数组引用、sizeCtl等)都使用 volatile修饰。

​作用​:确保了多线程之间的内存可见性。一个线程修改了这些变量后,其他线程能立即看到最新值,避免了脏读。这是实现无锁读取的基础。

【3】关键操作原理​

(1)put / putVal 操作​:CAS和synchronized

(1)计算 key 的 hash,定位到桶。
(2)如果桶为空,CAS 插入,成功则退出。
(3)如果桶不为空,检查是否正在扩容(MOVED节点),是则协助扩容。
(4)否则,synchronized锁住桶的头节点。
(5)遍历链表或树:
1-如果 key 已存在,根据规则覆盖或保留旧值。
2-如果不存在,插入新节点。
(6)如果是链表,检查是否需要树化。
(7)最后,判断是否需要扩容,并增加计数。

(2)get 操作​:无锁

(1)完全无锁​!这是 CHM 高性能读的关键。
(2)由于 table数组和 Node的 value, next字段都是 volatile的,get操作可以直接读取这些数据而无需加锁,总能拿到最新的写入值。
(3)遍历链表或树并返回匹配的值。

(3)size() 操作​:CounterCell分段计数

(1)这是一个较难实现的操作,因为要在不停止所有线程的情况下统计一个不断变化的量。
(2)​实现​:CHM 没有像传统做法那样全局加锁计数,而是采用了一种称为 ​CounterCell​ 的分段计数方法(借鉴了 LongAdder的思想)。
(3)每个线程修改元素时,会尝试 CAS 更新一个基础计数器 baseCount。如果更新失败(表示有竞争),则会将计数累加到线程对应的 CounterCell中。
(4)size()方法最终返回 baseCount与所有 CounterCell中的值之和。这是一个最终一致性的近似值,在并发极高时可能不是完全精确的瞬时值,但性能极高。

(4)🔥扩容 (Transfer) 操作

基于JDK 8版本

这是 CHM 中最复杂的部分。当元素数量超过阈值时,数组会扩容为原来的 2 倍。

1-概述

(1)关键特性:多线程协助扩容。
(2)当一个线程触发扩容时,它会负责分配转移任务(将旧数组划分为多个“ stride ”块)。
(3)其他正在执行 put或 remove操作的线程如果检测到当前桶正处于转移状态(遇到一个特殊的 ForwardingNode节点),​不会阻塞等待,而是会协助完成转移工作。转移完成后,再继续完成自己的操作。
(4)这种设计极大地利用了并发性能,加快了扩容过程。​

2-扩容的触发时机​

CHM 会在两种主要情况下触发扩容(resize,在代码中常被称为 transfer):
(1)​容量超过阈值​:在插入新元素后,如果当前元素总数(通过 addCount方法计算)超过了

容量 * 负载因子(如 16 * 0.75 = 12)

则会触发扩容。
(2)链表转红黑树时的容量检查​:当链表长度达到 8,准备转换为红黑树前,会检查当前数组的容量。如果数组容量小于 64,会优先选择执行扩容(tryPresize),而不是转树,因为扩容可能减少哈希冲突。

3-扩容的核心思路​

CHM 扩容的核心思路是:​将一个旧的 table 数组中的键值对,迁移到一个新的、容量翻倍(2倍)的 newTable 数组中。

最大的挑战在于:如何让多个线程安全、高效地协同完成这个数据迁移任务,而不发生混乱、丢失数据或形成死循环。

【4】扩容的详细过程

扩容过程的详细步骤​

(1)步骤 1:初始化与规划 (单线程完成)​​

当一个线程(例如线程 T1)执行 put操作并发现需要扩容时:
(1)​分配任务块 (Stride)​​:T1 并不会自己包揽所有工作,而是将旧的 table 数组逻辑上划分成多个小的、连续的任务块。每个任务块包含一定数量的桶(比如最少16个桶为一个步长stride)。
(2)​创建新数组​:T1 会创建一个新的、容量是旧数组两倍的 nextTable数组。
(3)设置控制变量 sizeCtl​:这是一个非常重要的 volatile变量,用于控制整个扩容过程的状态。T1 会将其设置为一个负数(例如 -1),表示自己正在初始化扩容。成功后,会将其设置为 (新数组长度的) * 0.75,并让其最高位为负,表示扩容正在进行中,其低位数代表了参与扩容的线程数。
(4)​放置“路标”:ForwardingNode​:T1 会先为自己要处理的第一个桶的位置放置一个特殊的节点——ForwardingNode。这个节点是关键中的关键,它有两大作用:
1-标识作用​:告诉所有其他线程:“这个桶里的数据已经被搬走了,现在正在扩容中,别动它”。
2-​路由作用​:如果在此期间有查询请求 (get) 到达,遇到 ForwardingNode,查询操作会被转发到新的 nextTable数组中去执行,从而保证在扩容过程中,读操作永远不会被阻塞,并且能拿到正确的结果。

至此,T1 完成了扩容的初始化工作。

(2)步骤 2:多线程协助迁移 (并发完成)​​

现在,其他线程(例如 T2, T3…)在干什么?

1-​场景一:其他线程也来执行 put或 remove​

当这些线程根据 key 的 hash 找到某个桶时,如果发现这个桶的头节点是 ForwardingNode,它们会立刻明白:“哦,当前正在扩容中”。
它们不会傻等着扩容结束,而是会主动调用 helpTransfer()方法参与到扩容工作中来。这是一个“人人为我,我为人人”的设计。

2-​场景二:协助过程​

(1)​领取任务​:协助的线程会通过检查一个共享的扩容进度标识​(例如 transferIndex)来“领取”一个尚未被处理的任务块(stride)。

(2)​处理自己的任务块​:线程会锁住(synchronized)自己任务块内的每一个桶的头节点,然后开始迁移该桶中的链表或树到新数组。
1-迁移算法 (非常重要)​​:CHM 采用的算法非常高效。由于新数组大小是旧数组的 2 倍(n -> 2n),所以每个桶中的元素在新数组中的位置要么是原来的位置 i,要么是 i + n。
2-例如,旧数组长度是 16,桶 i=5中的元素,在新数组(长度32)中,只可能出现在 5或 5+16=21这两个位置。
3-线程会遍历链表或树,根据哈希值的新比特位(第 log2(n)位)是 0 还是 1,将节点分成低位链和高位链。低位链放在新数组的 i位置,高位链放在新数组的 i + n位置。
4-这个过程保持了节点的相对顺序​(JDK 8 是尾插法),因此绝对不会出现 HashMap JDK 7 那样的死循环问题。

(3)​放置路标​:一个桶迁移完成后,线程会在旧数组的这个桶位置放置一个 ForwardingNode,标记该桶已处理完毕。

(4)​更新进度​:当一个线程完成自己领取的任务块后,它会继续领取下一个未处理的任务块,直到所有桶都被迁移完毕。

(3)步骤 3:收尾与切换 (单线程完成)​​

当所有桶都被成功迁移后,最终会有一个线程(可能是最初的 T1,也可能是其他协助线程)来完成收尾工作:
(1)table 引用切换​:将 ConcurrentHashMap内部的 table引用从旧的数组指向新的 nextTable数组。
(2)更新 sizeCtl​:将 sizeCtl的值设置为 (新容量 * 负载因子),恢复为正数,表示扩容结束,下一次的扩容阈值也同时设好了。
(3)丢弃旧数组​:旧的数组可以被垃圾回收了。
至此,整个扩容过程圆满结束。

(4)核心机制与设计哲学​

(1)​化整为零​:将庞大的扩容任务分解成许多小任务块 (stride),允许并行处理。
(2)​协同工作 (Help Transfer)​​:任何访问到正在扩容的 Map 的线程,都有义务协助完成迁移。这极大地加快了扩容速度,避免了单个线程迁移缓慢成为瓶颈。
(3)无锁读 + 锁桶写​:
1-​读操作 (get)​​:永远不会被阻塞。即使遇到 ForwardingNode,也会去新数组查找。这是高可用性的体现。
2-写操作 (put/remove)​​:在迁移某个桶时,会对该桶加锁(synchronized),阻塞其他想要修改这个桶的写操作,但不会阻塞读操作,也不会阻塞其他线程处理其他桶。
(4)​状态可见性​:通过 volatile变量 sizeCtl和 table引用,确保所有线程能立刻感知到扩容状态的变化。
(5)ForwardingNode 的神奇作用​:它是协调读操作、写操作和扩容操作的“交通警察”,是整个流程能无缝衔接的核心设计。

(5)总结

ConcurrentHashMap的扩容过程是一个极其精巧的分布式协作算法。它通过任务分治、协同搬运、状态标记和细粒度锁,完美地解决了并发环境下哈希表扩容这一难题。

与 HashMap的单线程、阻塞式扩容相比,CHM 的扩容:
(1)​效率极高​:多线程并行搬运数据。
(2)不影响服务​:读操作完全不受影响,写操作只在被处理的桶上短暂阻塞。
(3)绝对安全​:不会出现数据丢失或死循环。
这种设计是 ConcurrentHashMap能够成为高性能并发编程基石的重要原因之一。

【三】ConcurrentHashMap 与 HashMap 的全面区别​

在这里插入图片描述

【四】总结与使用建议

总结与使用建议​
(1)​单线程程序​:毫无疑问使用 ​HashMap,它的性能是最好的。
(2)多线程程序,需要读写 Map​:
1-​JDK 5+​​:使用 ​ConcurrentHashMap。它是替代旧的 Hashtable和 Collections.synchronizedMap(map)的最佳选择。
2-​读多写少​:ConcurrentHashMap的无锁读特性使其表现极佳。
3-写多​:ConcurrentHashMap的细粒度锁仍然能提供很高的吞吐量。
4-​需要强一致性的迭代​:如果需要在迭代时获取绝对最新的、且迭代过程中不变的数据视图,ConcurrentHashMap的弱一致性迭代器可能不满足要求。此时可能需要考虑加外部锁,但这种情况很少见。
5-需要存储 null 值​:如果业务必须使用 null,则不能使用 ConcurrentHashMap,只能使用 HashMap并自行处理线程安全。

​核心思想​:ConcurrentHashMap通过结合 ​CAS(无锁)、synchronized(细粒度锁)、volatile(内存可见性)​​ 这三种技术,以及精巧的多线程协作扩容设计,在线程安全与高性能之间取得了完美的平衡,成为了 Java 并发编程中最重要的集合类之一。

【五】补充扩展:redis的Hash如何实现扩容

Redis 的 Hash 扩容机制可以概括为:​渐进式 Rehash (Incremental Rehashing)​。

【1】核心数据结构​

要理解扩容,首先需要了解 Redis 字典 (dict) 的核心结构:

(1)dict结构体​:代表整个字典。

(1)ht[2]:​两个哈希表​ (dictht)。通常情况下,只使用 ht[0],ht[1]在 rehash 时使用。
(2)rehashidx:​Rehash 索引。当不在 rehash 时,值为 -1;当在 rehash 时,其值表示 ht[0]中正在进行 rehash 的桶的索引。
(3)iterators:当前正在运行的迭代器数量。

(2)dictht结构体​(哈希表):

(1)table:一个指针数组,每个元素指向一个 dictEntry(键值对节点)链表。这就是桶数组。
(2)size:哈希表的大小,即桶数组的长度。​总是 2^n。
(3)sizemask:掩码,用于计算索引值,总是 size - 1。
(4)used:哈希表中已有的键值对数量。

(3)dictEntry结构体​

代表一个键值对,同时包含指向下一个 dictEntry的指针,用于解决哈希冲突(链地址法)。

/* 简化后的核心数据结构 */
typedef struct dict {dictht ht[2];      // 两个哈希表,交替使用long rehashidx;    // Rehash 的进度,-1 表示未进行// ... 其他字段如类型特定函数等
} dict;typedef struct dictht {dictEntry **table; // 指向桶数组的指针unsigned long size;unsigned long sizemask;unsigned long used;
} dictht;typedef struct dictEntry {void *key;void *val;struct dictEntry *next; // 指向下一个Entry,形成链表
} dictEntry;

初始状态下,ht[0]是一个空表,ht[1]为 NULL,rehashidx = -1。

【2】扩容的触发时机​

Redis 在每一次执行添加、查找、删除等命令前,都会检查是否需要 rehash。触发扩容有两个条件,只要满足任意一个即可:

(1)负载因子触发 (主要条件)​​

(1)​条件​:负载因子 = ht[0].used / ht[0].size > 1。
(2)​例外​:如果 Redis 正在执行 BGSAVE或 BGREWRITEAOF等持久化子进程操作,为了减少父进程的内存压力(避免 Copy-On-Write 机制导致过多内存重复),Redis 会变得更加保守。此时,只有当负载因子大于 5时才会触发扩容。这个阈值可以通过 redis.conf中的 activerehashing参数调整。

(2)无法完成命令触发 (强制条件)​​:

当 ht[0].size已经非常大,但负载因子还未达到阈值时,如果服务器尝试执行一个命令,并且发现当前哈希表没有足够的连续空闲空间​(可能因为有很多删除操作导致碎片化)来完成该操作,则会立即触发扩容。

【3】扩容过程:渐进式 Rehash (核心)​​

与一次性将所有键值对从旧表迁移到新表(会阻塞主线程)不同,Redis 采用了一种分而治之、平摊成本的方法。

(1)​步骤 1:空间分配​

为 ht[1]分配一个新的、更大的哈希表。​新大小的计算规则是:大于 ht[0].used * 2的第一个 2^n。

例如,假设 ht[0].used = 5,那么 5 * 2 = 10。比 10 大的第一个 2^n 是 16。所以新 ht[1]的 size为 16。

(2)​步骤 2:开始 Rehash​

将 dict结构中的 rehashidx字段从 -1设置为 0。​这标志着 rehash 正式开始了。

(3)​步骤 3:逐步迁移 (最精妙的部分)​​

(1)在接下来的每次对字典的增、删、改、查操作中,Redis 除了执行指定的命令外,还会顺带做一件额外的事:
1-检查 rehashidx是否不等于 -1(即是否在 rehash 中)。
2-如果是,则它将从 ht[0].table[rehashidx]开始,将这个桶上的所有键值对重新哈希 (rehash)​​ 到 ht[1]中。
3-迁移完后,将 ht[0].table[rehashidx]设置为 NULL。
4-将 rehashidx的值加 1,指向下一个需要迁移的桶。

​这个过程完美地将庞大的迁移工作分摊到了无数个细小的操作中。每个操作只迁移一个桶,避免了单次操作时间过长导致阻塞 Redis 的单线程主循环。

(4)​步骤 4:定时任务辅助​

由于 Redis 可能很长一段时间没有收到请求,导致 rehash 进度停滞。为了解决这个问题,Redis 的时间事件处理器 (serverCron)​​ 会定期(默认每 100 毫秒)检查并执行 rehash 操作。

它会在一个规定的时间片(比如 1 毫秒)内,连续迁移多个桶,直到时间片用完或所有桶都迁移完毕。

(5)​步骤 5:完成与切换​

(1)当 ht[0]的所有桶都被迁移完毕,即 rehashidx的值超过了 ht[0].size。
(2)释放 ht[0]的空间。
(3)将 ht[1]设置为新的 ht[0]。
(4)为 ht[1]分配一个 NULL空表,为下一次 rehash 做准备。
(5)将 rehashidx重新设置为 -1,标志着本次 rehash 完成。

【4】在此期间的操作如何执行?​​

在 rehash 进行期间,字典同时持有两个哈希表 ht[0]和 ht[1]。所有操作都需要在两个表中进行。

(1)​查找 (GET, HGET)​​:

(1)先到 ht[0]中查找。
(2)如果没找到,再去 ht[1]中查找。

(2)​添加 (HSET, HMSET)​​:

​新添加的键值对会直接被插入到 ht[1]中。这保证了 ht[0]的键值对数量只会减少不会增加,使 rehash 过程最终一定能完成。

(3)删除 (HDEL)​​:

会同时在 ht[0]和 ht[1]中查找并删除对应的键。

【5】总结与优势

在这里插入图片描述
与 Java HashMap 的对比​:
(1)​Java HashMap​:在单线程下,一次性完成扩容,期间该次 put操作耗时较高。
(2)Redis Dict​:在任何情况下都采用渐进式 rehash,将耗时平摊,​极致追求服务的稳定性和响应速度,这是作为数据库中间件的核心要求。

这种精巧的设计是 Redis 能够实现高性能、单线程模型却又能处理大量数据的基石之一。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/920308.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/920308.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android 中使用开源库 ZXing 生成二维码图片

在 Android 中生成二维码是一个比较常见的功能&#xff0c;可以使用开源库 ZXing&#xff08;Zebra Crossing&#xff09;库来实现&#xff0c;这是一个非常流行的二维码生成和扫描库。 1、添加依赖库 在 app/build.gradle.kt 中添加依赖库。 dependencies { ......implementat…

vue 如何使用 vxe-table 来实现跨表拖拽,多表联动互相拖拽数据

vue 如何使用 vxe-table 来实现跨表拖拽&#xff0c;多表联动互相拖拽数据 row-drag-config.isCrossTableDrag 启用跨表格、多表格互相拖拽&#xff1b;跨表拖拽需要确保数据主键不重复&#xff0c;通过 row-config.keyField 指定主键字段名 查看官网&#xff1a;https://vxe…

微生产力革命:AI解决生活小任务分享会

微生产力革命的概念微生产力革命指利用AI技术高效解决日常琐碎任务&#xff0c;释放时间与精力。其核心在于将重复性、低价值的事务自动化&#xff0c;聚焦创造性或高价值活动。AI解决生活小任务的典型场景健康管理 AI健身助手可定制个性化训练计划&#xff0c;通过摄像头实时纠…

标量、向量、矩阵和张量的区别

注&#xff1a;本文为 “标量、向量、矩阵和张量的区别” 相关合辑。 英文引文&#xff0c;机翻未校。 如有内容异常&#xff0c;请看原文。 Difference Between Scalar, Vector, Matrix and Tensor 标量、向量、矩阵和张量的区别 Last Updated : 06 Aug, 2025 In the conte…

VScode,设置自动保存

在搜索框输入“autoSave”或VSCode提供以下自动保存选项&#xff1a; 在搜索框输入“autoSave” Off&#xff1a;禁用自动保存。 On Focus Change&#xff1a;当您将焦点从编辑器移开时自动保存。 On Window Change&#xff1a;当您切换窗口选项卡或编辑器时自动保存。 After D…

2025.8.27链表_链表逆置

链表中的指针只是用来标记&#xff0c;具体连接方式&#xff0c;是按照node.next链接。JAVA中头节点存东西&#xff0c;不是空的。核心原理&#xff1a;Java 的参数传递是"值传递"&#xff0c;但对象引用是"值传递引用"也就是传过来了ListNode head。headh…

ssc37x平台的音频应用demo

//ao_test.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include

PPT处理控件Aspose.Slides教程:在.NET中开发SVG到EMF的转换器

SVG和EMF都是基于矢量的格式。许多传统的 CAD 和报告工具仍然倾向于使用 EMF 文件格式&#xff0c;因为它具有更广泛的兼容性。如果您正在开发一个 .NET 项目&#xff0c;并希望实现自动化&#xff0c;使 SVG 到 EMF 的转换变得轻松便捷。Aspose.Slides for .NET是一个功能强大…

深入理解HTTP:请求、响应与状态码解析

深入理解HTTP&#xff1a;请求、响应与状态码解析一&#xff1a;概述二&#xff1a;协议版本三&#xff1a;协议详解1&#xff09;请求报文2&#xff09;响应报文四&#xff1a;状态码1&#xff09;1xx&#xff1a;信息状态码2&#xff09;2xx&#xff1a;成功状态码3&#xff…

浏览器输入网址回车后,访问网页全流程解析!

你在地址栏敲下 https://baidu.com.com 并回车&#xff0c;几百毫秒内发生了很多事&#xff1a;浏览器先想“这个域名的 IP 我记得吗”&#xff0c;接着去找 DNS&#xff1b;建立连接时还要握个手&#xff08;TCP/QUIC&#xff09;顺便打个招呼&#xff08;TLS 证书校验、ALPN …

[Linux]学习笔记系列 -- mm/percpu

文章目录mm/percpu.c Per-CPU Variables Management Per-CPU数据管理的核心实现历史与背景这项技术是为了解决什么特定问题而诞生的&#xff1f;它的发展经历了哪些重要的里程碑或版本迭代&#xff1f;目前该技术的社区活跃度和主流应用情况如何&#xff1f;核心原理与设计它的…

企微会话风控「智脑」:源雀SCRM的AI合规守护引擎

一&#xff1a;功能概述源雀SCRM会话风控功能是一款专为企业客户关系管理设计的智能风控解决方案&#xff0c;通过双重审计机制保障企业会话合规性&#xff0c;发送违规内容及时提醒通知企业负责人。二&#xff1a;核心功能1. 普通风控审计基于Lucene全文关键词检索&#xff1a…

Java岗位供过于求,如何破局?

程序员面试背八股&#xff0c;可以说是现在互联网开发岗招聘不可逆的形式了&#xff0c;其中最卷的当属Java&#xff01;&#xff08;网上动不动就是成千上百道的面试题总结&#xff09;你要是都能啃下来&#xff0c;平时技术不是太差的话&#xff0c;面试基本上问题就不会太大…

白话FNN、RNN、Attention和self-attention等

我尝试同过炸酱面的例子,让所有人都能理解Transformer的核心Self-Attention。你在做老北京炸酱面的酱,食谱包括一系列步骤:准备食材(干黄酱、甜面酱、猪肉、葱姜等)、洗菜、切菜(葱姜蒜等)、炒肉,调和干黄酱、甜面酱、凉水、酱油后,加入锅中,慢炖成酱。我们将从FNN开…

路由基础(一):IP地址规划

###IPv4地址 IPv4地址分成网络部分和主机部分 IPv4地址根据定义可分为&#xff1a; A类地址 a. 第一字节的第一位为0 b. 第一字节的数值范围为1-126B类地址 a. 第一字节的第一和第二位为10的一组地址 b. 第一字节的数值范围为128-191C类地址 a. 第一字节的第一、第二和第三位为…

Content-Type是application/x-www-form-urlencoded表示从前端到后端提交的是表单的形式

Content-Type: application/x-www-form-urlencoded 就是表示前端向后端提交的是表单&#xff08;form&#xff09;数据的形式。✅ 精确解释&#xff1a;这个 Content-Type 是 HTML 表单&#xff08;form&#xff09;默认的提交编码方式&#xff0c;它的名字就可以拆解理解&…

一、添加Viewport3DX,并设置相机、灯光

后续主要介绍使用高性能Wpf.SharpDX版本的使用。 其核心组件包括: Viewport3DX 控件:作为渲染视口,管理相机、场景元素、输入事件和渲染主机。 CameraController:封装相机交互逻辑,实现旋转、缩放、平移等操作。 RenderHost:SharpDX 的抽象,负责 GPU 渲染,支持多种渲染…

AI生成音乐模型发展现状与前景

第一章 引言与市场概述人工智能音乐生成技术正在经历一个前所未有的爆发期&#xff0c;从实验室的技术演示迅速发展为商业化的成熟产品。根据Digital Ocean 2025年的最新报告&#xff0c;全球AI音乐市场预计将从2023年的39亿美元增长到2033年的387亿美元&#xff0c;年复合增长…

Oh My Zsh + Tabby 终端配置指南

zsh Tabby 终端配置指南现代化终端环境搭建&#xff0c;提升开发效率的完整方案&#x1f3af; 方案概述 组合架构&#xff1a;Tabby (终端模拟器) zsh (Shell) Oh My Zsh (框架) Powerlevel10k (主题) 为什么选择这个组合&#xff1f; 跨平台统一&#xff1a;Windows/macOS…

宝石组合(蓝桥杯)

发现规律很重要&#xff0c;推荐这篇文章 讲解<——————看这位大佬的讲解&#xff0c;很清楚 &#xff08;在文末想和聪明的你讨论一个问题&#xff0c;盼望您的讨论与解答&#xff09; #include <iostream> #include <vector> #include <algorithm&…