ConcurrentHashMap在JDK1.7和JDK1.8的实现对比

本文对比分析了Java 7和Java 8中ConcurrentHashMap的实现,Java 7采用Segment分段锁,Java 8则用数组+链表+红黑树,提升了并发度和查询效率。在Java 8中,当链表长度超过8时会转为红黑树,以保持O(log(n))的查找性能。同时,文章深入解析了Java 8的put和get方法的源码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java 8 中,对于 ConcurrentHashMap 这个常用的工具类进行了很大的升级,对比之前 Java 7 版本在诸多方面都进行了调整和变化。不过,在 Java 7 中的 Segment 的设计思想依然具有参考和学习的价值,本博客就对 ConcurrentHashMap 在这两个版本的特点和性质进行对比和介绍。

Java 7 版本的 ConcurrentHashMap

我们首先来看一下 Java 7 版本中的 ConcurrentHashMap 的结构示意图:

在这里插入图片描述
从图中我们可以看出,在 ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁。

每个 Segment 的底层数据结构与 HashMap 类似,仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

Java 8 版本的 ConcurrentHashMap

在 Java 8 中,几乎完全重写了 ConcurrentHashMap,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行,所以也大大提高了源码的阅读难度。而为了方便我们理解,我们还是先从整体的结构示意图出发,看一看总体的设计思路,然后再去深入细节。

在这里插入图片描述
图中的节点有三种类型:

  • 第一种是最简单的,空着的位置代表当前还没有元素来填充。
  • 第二种就是和 HashMap 非常类似的拉链法结构,在每一个槽中会首先填入第一个节点,但是后续如果计算出相同的 Hash 值,就用链表的形式往后进行延伸。
  • 第三种结构就是红黑树结构,这是 Java 7 的 ConcurrentHashMap 中所没有的结构,在此之前我们可能也很少接触这样的数据结构。

当第二种情况的链表长度大于某一个阈值(默认为 8),且同时满足一定的容量要求的时候,ConcurrentHashMap 便会把这个链表从链表的形式转化为红黑树的形式,目的是进一步提高它的查找性能。所以,Java 8 的一个重要变化就是引入了红黑树的设计,由于红黑树并不是一种常见的数据结构,所以我们在此简要介绍一下红黑树的特点。

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色,红黑树的本质是对二叉查找树 BST 的一种平衡策略,我们可以理解为是一种平衡二叉查找树,查找效率高,会自动平衡,防止极端不平衡从而影响查找效率的情况发生。

由于自平衡的特点,即左右子树高度几乎一致,所以其查找性能近似于二分查找,时间复杂度是 O(log(n)) 级别;反观链表,它的时间复杂度就不一样了,如果发生了最坏的情况,可能需要遍历整个链表才能找到目标元素,时间复杂度为 O(n),远远大于红黑树的 O(log(n)),尤其是在节点越来越多的情况下,O(log(n)) 体现出的优势会更加明显。

红黑树的一些特点:

  • 每个节点要么是黑色,要么是红色。
  • 根节点是黑色。
  • 每个叶子节点(NIL)是黑色。
  • 每个红色结点的两个子结点一定都是黑色。
  • 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

正是由于这些规则和要求的限制,红黑树保证了较高的查找效率,所以现在就可以理解为什么 Java 8 的 ConcurrentHashMap 要引入红黑树了。好处就是避免在极端的情况下冲突链表变得很长,在查询的时候,效率会非常慢。而红黑树具有自平衡的特点,所以,即便是极端情况下,也可以保证查询效率在 O(log(n))。

分析Java 8 版本ConcurrentHashMap的重要源码

前面我们讲解了 Java 7 和 Java 8 中 ConcurrentHashMap 的主体结构,下面我们深入源码分析。由于 Java 7 版本已经过时了,所以我们把重点放在 Java 8 版本的源码分析上。

Node 节点

我们先来看看最基础的内部存储结构 Node,这就是一个一个的节点,如这段代码所示:

static class Node<K,V> implements Map.Entry<K,V> {
   
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    // ...
}

可以看出,每个 Node 里面是 key-value 的形式,并且把 value 用 volatile 修饰,以便保证可见性,同时内部还有一个指向下一个节点的 next 指针,方便产生链表结构。

下面我们看两个最重要、最核心的方法。

put 方法源码分析

put 方法的核心是 putVal 方法,为了方便阅读,我把重要步骤的解读用注释的形式补充在下面的源码中。我们逐步分析这个最重要的方法,这个方法相对有些长,我们一步一步把它看清楚。

final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    if (key == null || value == null) {
   
        throw new NullPointerException();
    }
    //计算 hash 值
    int hash = spread(key.hashCode(
### JDK 1.7 JDK 1.8ConcurrentHashMap 的区别 #### 数据结构变化 在 JDK 1.7 版本中,`ConcurrentHashMap` 使用分段锁定机制来提高并发性能。整个哈希表被分割成多个独立的部分(Segment),每个 Segment 类似于一个小型的 HashMap,并且有自己独立的锁。这使得不同部分可以并行访问而不会相互阻塞。 而在 JDK 1.8 及之后版本里,为了进一步提升性能减少内存占用,引入了一种新的设计——基于 CAS (Compare And Swap) 操作以及链表转红黑树优化策略。当桶内元素数量超过一定阈值时会自动转换为红黑树形式存储键值对,从而提高了查找速度[^4]。 #### 锁粒度调整 由于采用了更细粒度的数据分区方式,在处理高并发场景下能够显著降低争用率;同时通过无锁算法实现了大部分操作无需加锁即可完成,特别是对于只读请求来说几乎不存在同步开销问题[^2]。 ```java // JDK 1.7 示例代码片段 public V get(Object key) { int hash = hash(key); Segment<K,V> s = segmentFor(hash); // 获取对应的segment对象 return s.get(key, hash); // 调用segment内部方法获取value } // JDK 1.8 示例代码片段 final V doGet(Object key, int hash) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; if ((tab = table) != null && (n = tab.length) > 0 && (e = tab[(n - 1) & hash]) != null) { ... } } ``` #### 性能改进 随着新架构的应用,JDK 1.8 下 `ConcurrentHashMap` 表现出更好的扩展性更低延迟特性。特别是在多核处理器环境中运行大型应用程序时优势明显,测试结果显示其吞吐量远高于旧版实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值