图解ConcurrentHashMap数据结构设计与应用案例

216 阅读5分钟

image.png

ConcurrentHashMap 是 Java 中的一个线程安全的 Map 实现,它提供了高效的并发访问能力,适用于高并发场景。与传统的 HashMap 相比,ConcurrentHashMap 通过使用分段锁(Segment)机制来减少线程竞争,从而提高了多线程环境下的性能。它支持完全可配置的哈希函数、分段锁以及多种原子操作,使得在保持线程安全的同时,还能提供接近于 HashMap 的性能。ConcurrentHashMap 在 Java 8 中进行了优化,采用了 CAS 操作和红黑树来处理哈希冲突,进一步提高了性能。

1、 ConcurrentHashMap

设计思考:
  1. 需求场景
    • 在多线程应用中,需要快速地进行键值对的插入、删除和查找操作,同时保证线程安全。例如,在缓存系统、任务分配、实时数据处理等场景中,这些操作非常常见。
    • 适用于需要高并发访问和更新键值对的场景,如高并发网站后端、大数据处理等。
  2. 现有技术局限性
    • Hashtable 提供了线程安全的 Map 实现,但它使用全局锁来同步所有操作,这在高并发环境下会导致性能瓶颈。
    • HashMap 提供了非线程安全的快速查找性能,但需要额外的同步措施来保证线程安全,这同样会影响并发性能。
  3. 技术融合
    • ConcurrentHashMap 结合了 HashMap 的快速查找特性和线程安全的机制,使用分段锁(Segmentation)或 CAS(Compare-And-Swap)操作来实现高效的并发控制。
  4. 设计理念
    • ConcurrentHashMap 旨在提供一个线程安全的 Map,它通过减少锁的粒度或避免使用锁来提高并发性能,允许多个线程同时执行操作而不会相互阻塞。
  5. 实现方式
    • 在 Java 7 及之前版本中,ConcurrentHashMap 使用分段锁机制,将 Map 分成多个段(Segment),每个段有自己的锁,从而允许在不同段上并发执行操作。
    • 在 Java 8 及以后版本中,ConcurrentHashMap 使用 CAS 操作和 synchronized 关键字来实现无锁或细粒度锁定的并发控制,同时使用了红黑树来优化长链表的性能。
2、数据结构

image.png

图说明:
  • ConcurrentHashMap:表示 ConcurrentHashMap 类的实例,是一个线程安全的 Map 实现。
  • Segment Array:ConcurrentHashMap 内部使用一个段数组来存储数据,每个段是一个独立的哈希表。
  • Segment 1 & Segment 2:每个段都是一个独立的哈希表,相当于将整个哈希表分成多个小的片段,不同的段可以由不同的线程独立操作。
  • Hash Array:每个段包含一个哈希数组,用于存储桶(Buckets)。
  • Bucket 1 & Bucket 2:哈希数组中的每个桶可以包含一个或多个节点(Node),在 Java 8 及以后的版本中,当链表长度超过一定阈值时,链表会转换为红黑树。
  • Node:每个桶包含多个节点,这些节点存储实际的键值对,并且通过链表连接以维护顺序。
  • Key & Value:节点中存储的键和值。
  • Next Node:节点中的引用,指向链表中的下一个节点。
  • Null:链表的末尾节点的 Next Node 指向 Null。
  • Load Factor:加载因子,用于决定何时需要扩容哈希表。
3、 执行流程

image.png

图说明
  • 创建 ConcurrentHashMap 实例:初始化 ConcurrentHashMap 对象。
  • 插入元素(put) :执行将键值对插入到 ConcurrentHashMap 的操作。
  • 计算键的哈希码:计算插入键的哈希码以确定其在桶数组中的位置。
  • 确定段和桶位置:根据哈希码确定段和桶的位置。
  • 尝试插入节点:尝试在桶中插入节点,如果需要同步,则获取段锁。
  • 获取段锁:获取对应段的锁。
  • 插入或更新节点:在桶中插入新节点或更新现有节点。
  • 释放段锁:释放段锁。
  • 查找元素(get) :执行根据键查找值的操作。
  • 读取节点:读取对应桶中的节点。
  • 返回节点值:返回节点的值。
  • 删除元素(remove) :执行根据键删除键值对的操作。
  • 删除节点:从桶中删除节点。
  • 重新平衡树:删除节点后,可能需要对树进行重新平衡。

4、优点

  1. 线程安全
    • 提供了线程安全的 Map 操作,无需额外的同步措施。
  2. 高效的并发性能
    • 通过减少锁的粒度或避免使用锁,提高了高并发环境下的性能。
  3. 灵活的配置
    • 可以根据应用需求调整并发级别,以优化性能。

5、缺点

  1. 内存开销
    • 相比于 HashMap,ConcurrentHashMap 可能需要更多的内存来存储同步控制结构。
  2. 可能的ABA问题
    • 在使用 CAS 操作时,可能会遇到 ABA 问题,需要额外的原子变量类如 AtomicStampedReference 来解决。

6、使用场景

  • 高并发的键值对存储
    • 适用于需要快速插入、删除和查找键值对的场景,如缓存系统、任务分配等。
  • 需要线程安全的 Map 操作
    • 在多线程环境中,需要保证数据的一致性和完整性。

7、类设计

image.png

8、应用案例

ConcurrentHashMap 通常用于实现线程安全的缓存或计数器。以下是一个使用 ConcurrentHashMap 的真实业务案例,这是一个用户访问计数器系统,用于统计不同用户的访问次数:

import java.util.concurrent.ConcurrentHashMap;

public class UserAccessCounter {
    // 使用 ConcurrentHashMap 来存储用户访问次数
    private ConcurrentHashMap<String, Integer> userAccessCounts;

    public UserAccessCounter() {
        userAccessCounts = new ConcurrentHashMap<>();
    }

    // 增加用户的访问次数
    public void incrementUserAccess(String username) {
        // 使用 computeIfAbsent 方法来原子地更新访问次数
        userAccessCounts.computeIfAbsent(username, k -> 0).incrementAndGet();
    }

    // 获取用户的访问次数
    public int getUserAccess(String username) {
        // 返回指定用户的访问次数,如果用户不存在则返回 0
        return userAccessCounts.getOrDefault(username, 0);
    }

    // 主方法,用于演示 UserAccessCounter 的使用
    public static void main(String[] args) {
        UserAccessCounter counter = new UserAccessCounter();

        // 用户访问
        counter.incrementUserAccess("user1");
        counter.incrementUserAccess("user1");
        counter.incrementUserAccess("user2");

        // 打印用户访问次数
        System.out.println("User1 access count: " + counter.getUserAccess("user1"));
        System.out.println("User2 access count: " + counter.getUserAccess("user2"));
    }
}