From d11cfa5387a7b73947fed1573b2aeb69932d92f7 Mon Sep 17 00:00:00 2001 From: Guide Date: Sun, 29 Dec 2024 13:24:22 +0800 Subject: [PATCH] =?UTF-8?q?[docs=20update]=E5=AE=8C=E5=96=84=E8=A1=A5?= =?UTF-8?q?=E5=85=85HashMap=E3=80=81=E5=B9=B6=E5=8F=91=E9=9B=86=E5=90=88?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/redis/redis-data-structures-02.md | 2 +- docs/database/redis/redis-questions-01.md | 2 +- .../collection/java-collection-questions-02.md | 15 ++++++++++++++- .../concurrent/java-concurrent-collections.md | 12 +++++++++--- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/database/redis/redis-data-structures-02.md b/docs/database/redis/redis-data-structures-02.md index 8eb75f89..9e5fbcee 100644 --- a/docs/database/redis/redis-data-structures-02.md +++ b/docs/database/redis/redis-data-structures-02.md @@ -125,7 +125,7 @@ HyperLogLog 相关的命令非常少,最常用的也就 3 个。 **数量巨大(百万、千万级别以上)的计数场景** -- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、 +- 举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计。 - 相关命令:`PFADD`、`PFCOUNT` 。 ## Geospatial (地理位置) diff --git a/docs/database/redis/redis-questions-01.md b/docs/database/redis/redis-questions-01.md index e4ab42bd..3bed4651 100644 --- a/docs/database/redis/redis-questions-01.md +++ b/docs/database/redis/redis-questions-01.md @@ -146,7 +146,7 @@ Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特 - **消息队列**:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。 - **延时队列**:Redisson 内置了延时队列(基于 Sorted Set 实现的)。 - **分布式 Session** :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。 -- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。 +- **复杂业务场景**:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜、通过 HyperLogLog 统计网站 UV 和 PV。 - …… ### 如何基于 Redis 实现分布式锁? diff --git a/docs/java/collection/java-collection-questions-02.md b/docs/java/collection/java-collection-questions-02.md index 931161bf..94eafcf9 100644 --- a/docs/java/collection/java-collection-questions-02.md +++ b/docs/java/collection/java-collection-questions-02.md @@ -222,10 +222,23 @@ static int hash(int h) { #### JDK1.8 之后 -相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树。 + +这样做的目的是减少搜索时间:链表的查询效率为 O(n)(n 是链表的长度),红黑树是一种自平衡二叉搜索树,其查询效率为 O(log n)。当链表较短时,O(n) 和 O(log n) 的性能差异不明显。但当链表变长时,查询性能会显著下降。 ![jdk1.8之后的内部结构-HashMap](https://oss.javaguide.cn/github/javaguide/java/collection/jdk1.8_hashmap.png) +**为什么优先扩容而非直接转为红黑树?** + +数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。 + +红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。 + +**为什么选择阈值 8 和 64?** + +1. 泊松分布表明,链表长度达到 8 的概率极低(小于千万分之一)。在绝大多数情况下,链表长度都不会超过 8。阈值设置为 8,可以保证性能和空间效率的平衡。 +2. 数组长度阈值 64 同样是经过实践验证的经验值。在小数组中扩容成本低,优先扩容可以避免过早引入红黑树。数组大小达到 64 时,冲突概率较高,此时红黑树的性能优势开始显现。 + > TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。 diff --git a/docs/java/concurrent/java-concurrent-collections.md b/docs/java/concurrent/java-concurrent-collections.md index 9a669d90..45aa2588 100644 --- a/docs/java/concurrent/java-concurrent-collections.md +++ b/docs/java/concurrent/java-concurrent-collections.md @@ -15,13 +15,19 @@ JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 ## ConcurrentHashMap -我们知道 `HashMap` 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 `HashMap`。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。 +我们知道,`HashMap` 是线程不安全的,如果在并发场景下使用,一种常见的解决方式是通过 `Collections.synchronizedMap()` 方法对 `HashMap` 进行包装,使其变为线程安全。不过,这种方式是通过一个全局锁来同步不同线程间的并发访问,会导致严重的性能瓶颈,尤其是在高并发场景下。 -所以就有了 `HashMap` 的线程安全版本—— `ConcurrentHashMap` 的诞生。 +为了解决这一问题,`ConcurrentHashMap` 应运而生,作为 `HashMap` 的线程安全版本,它提供了更高效的并发处理能力。 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段(`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 -到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本。 +![Java7 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java7_concurrenthashmap.png) + +到了 JDK1.8 的时候,`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。 + +Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 + +![Java8 ConcurrentHashMap 存储结构](https://oss.javaguide.cn/github/javaguide/java/collection/java8_concurrenthashmap.png) 关于 `ConcurrentHashMap` 的详细介绍,请看我写的这篇文章:[`ConcurrentHashMap` 源码分析](./../collection/concurrent-hash-map-source-code.md)。