Interview AiBox logo

Interview AiBox 实时 AI 助手,让你自信应答每一场面试

download免费下载
高阶local_fire_department26 次面试更新于 2025-08-23account_tree思维导图

ConcurrentHashMap的实现原理是什么?

lightbulb

题型摘要

ConcurrentHashMap是Java并发包中的线程安全HashMap实现,旨在提供高并发性和高性能。JDK 1.7及之前采用分段锁(Segment)设计,每个Segment守护一部分数据,不同Segment的操作可并发执行。JDK 1.8及之后改为使用CAS操作和synchronized关键字,锁粒度更细,只锁住需要修改的桶,进一步提高了并发性。ConcurrentHashMap的读操作通常不需要加锁,写操作只锁定必要的部分,支持多线程并发扩容,整体性能远超Hashtable和Collections.synchronizedMap。其实现体现了无锁算法、细粒度锁、并发扩容等并发编程思想,是高性能并发编程的重要组件。

ConcurrentHashMap的实现原理

1. 基本概念和设计目标

ConcurrentHashMap是Java并发包(java.util.concurrent)中的一个线程安全的HashMap实现。它的设计目标是在保证线程安全的同时,提供比Hashtable和Collections.synchronizedMap更高的并发性能。

设计目标

  • 高并发性:允许多个线程同时读和一定程度的写操作
  • 线程安全:保证多线程环境下数据的一致性
  • 高性能:尽可能减少锁竞争,提高并发访问效率

2. JDK 1.7及之前的实现(分段锁)

在JDK 1.7及之前的版本中,ConcurrentHashMap采用**分段锁(Segment)**的设计来实现并发控制。

内部结构

ConcurrentHashMap内部由一个Segment数组组成,每个Segment又包含一个HashEntry数组。Segment是一种可重入锁(ReentrantLock),每个Segment守护着它所包含的HashEntry数组中的元素。

--- title: ConcurrentHashMap 1.7 内部结构 --- graph TD A["ConcurrentHashMap"] --> B["Segment数组"] B --> C["Segment 0"] B --> D["Segment 1"] B --> E["Segment 2"] B --> F["Segment N"] C --> G["HashEntry数组"] D --> H["HashEntry数组"] E --> I["HashEntry数组"] F --> J["HashEntry数组"] G --> K["HashEntry 0"] G --> L["HashEntry 1"] G --> M["HashEntry N"]

工作原理

  1. 数据定位

    • 首先通过key的hash值确定在哪个Segment中
    • 然后在该Segment中通过再次hash确定具体的HashEntry位置
  2. 并发控制

    • 写操作需要获取对应Segment的锁
    • 读操作不需要加锁(通过volatile保证可见性)
    • 不同Segment的写操作可以并发进行
  3. 容量计算

    • 整个ConcurrentHashMap的容量是所有Segment容量的总和
    • 每个Segment的容量默认为16,负载因子为0.75

代码示例

// JDK 1.7 ConcurrentHashMap的put方法简化版
public V put(K key, V value) {
    Segment<K,V> s;
    // 计算key的hash值,并找到对应的Segment
    int hash = hash(key.hashCode());
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 调用Segment的put方法
    return s.put(key, hash, value, false);
}

// Segment的put方法简化版
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 尝试获取锁,如果失败则自旋获取
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

优缺点

优点

  • 锁粒度细,并发度高
  • 在多线程环境下读写性能较好

缺点

  • 内存占用较高(每个Segment都维护自己的HashEntry数组)
  • 在某些场景下,并发度可能不够高(Segment数量固定)

3. JDK 1.8及之后的实现(CAS + synchronized)

从JDK 1.8开始,ConcurrentHashMap的实现发生了重大变化,放弃了分段锁的设计,转而采用CAS(Compare And Swap)操作和synchronized关键字来实现并发控制。

内部结构

JDK 1.8的ConcurrentHashMap内部结构与HashMap类似,由一个Node数组组成,每个Node可能是一个链表或红黑树的头节点。

--- title: ConcurrentHashMap 1.8 内部结构 --- graph TD A["ConcurrentHashMap"] --> B["Node数组"] B --> C["Node 0"] B --> D["Node 1"] B --> E["Node 2"] B --> F["Node N"] C --> G["链表或红黑树"] D --> H["链表或红黑树"] E --> I["链表或红黑树"] F --> J["链表或红黑树"] G --> K["Node"] G --> L["Node"] G --> M["Node"]

工作原理

  1. CAS操作

    • 在无竞争的情况下,使用CAS操作来更新节点的值
    • CAS是一种无锁算法,通过比较并交换来保证原子性
  2. synchronized锁

    • 在有竞争的情况下,使用synchronized锁住对应的桶(Node数组中的某个位置)
    • 锁的粒度更细,只锁住需要修改的桶,而不是整个Segment
  3. 关键优化

    • 扩容优化:支持多线程并发扩容
    • 树化优化:当链表长度超过8时,转换为红黑树,提高查询效率
    • 计数优化:使用CounterCell来记录元素数量,减少竞争

代码示例

// JDK 1.8 ConcurrentHashMap的putVal方法简化版
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        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;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 使用synchronized锁住桶的头节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

优缺点

优点

  • 内存占用更少(不需要维护多个Segment)
  • 锁粒度更细,并发度更高
  • 在高并发场景下性能更好

缺点

  • 实现更复杂,理解难度更高
  • 在某些低并发场景下,性能可能不如JDK 1.7的实现

4. 关键操作的实现原理

get操作

--- title: ConcurrentHashMap get操作流程 --- sequenceDiagram participant Thread as 线程 participant CHM as ConcurrentHashMap Thread->>CHM: get(key) CHM->>CHM: 计算hash值 CHM->>CHM: 定位到对应的桶 CHM->>CHM: 遍历链表或红黑树 CHM->>CHM: 返回找到的值 CHM-->>Thread: 返回结果
  1. JDK 1.7

    • 不加锁,通过volatile保证可见性
    • 首先定位到Segment,然后在Segment中查找对应的HashEntry
  2. JDK 1.8

    • 不加锁,通过volatile保证可见性
    • 直接通过hash值定位到对应的桶,然后遍历链表或红黑树

put操作

--- title: ConcurrentHashMap put操作流程 --- sequenceDiagram participant Thread as 线程 participant CHM as ConcurrentHashMap Thread->>CHM: put(key, value) CHM->>CHM: 计算hash值 CHM->>CHM: 定位到对应的桶 alt 桶为空 CHM->>CHM: 使用CAS操作添加新节点 else 桶不为空 CHM->>CHM: 使用synchronized锁住桶的头节点 CHM->>CHM: 遍历链表或红黑树 CHM->>CHM: 更新或添加节点 CHM->>CHM: 释放锁 end CHM->>CHM: 检查是否需要扩容或树化 CHM-->>Thread: 返回结果
  1. JDK 1.7

    • 首先定位到Segment,然后获取Segment的锁
    • 在Segment中执行put操作,可能触发rehash
    • 释放锁
  2. JDK 1.8

    • 首先定位到桶
    • 如果桶为空,使用CAS操作添加新节点
    • 如果桶不为空,使用synchronized锁住桶的头节点
    • 执行put操作,可能触发树化或扩容
    • 释放锁

size操作

  1. JDK 1.7

    • 遍历所有Segment,累加它们的count值
    • 在累加过程中,可能尝试获取Segment的锁(如果连续两次获取的值不一致)
  2. JDK 1.8

    • 使用CounterCell数组来记录元素数量
    • 在多线程环境下,不同线程可能更新不同的CounterCell
    • 计算size时,累加所有CounterCell的值

扩容操作

--- title: ConcurrentHashMap 扩容流程 --- flowchart TD A["开始扩容"] --> B["创建新的Node数组"] B --> C["将旧数组的元素迁移到新数组"] C --> D["是否有其他线程协助扩容?"] D -->|是| E["分配任务给其他线程"] D -->|否| F["当前线程完成所有迁移"] E --> G["多线程并发迁移"] G --> H["等待所有迁移完成"] F --> H H --> I["更新table引用"] I --> J["扩容完成"]
  1. JDK 1.7

    • 每个Segment独立扩容,不影响其他Segment
    • 扩容时获取Segment的锁,阻塞其他线程对该Segment的操作
  2. JDK 1.8

    • 支持多线程并发扩容
    • 扩容时,将数组分成多个部分,不同线程可以负责迁移不同的部分
    • 迁移过程中,读操作可以并发进行,写操作可能会协助扩容

5. 与其他线程安全Map的对比

--- title: 线程安全Map对比 --- graph TD A["线程安全Map对比"] --> B["Hashtable"] A --> C["Collections.synchronizedMap"] A --> D["ConcurrentHashMap"] B --> E["锁机制"] E --> F["对象锁(synchronized)"] B --> G["性能特点"] G --> H["写操作阻塞整个表"] G --> I["并发度低"] C --> J["锁机制"] J --> K["对象锁(synchronized)"] C --> L["性能特点"] L --> M["写操作阻塞整个表"] L --> N["并发度低"] D --> O["锁机制"] O --> P["JDK 1.7: 分段锁"] O --> Q["JDK 1.8: CAS + synchronized"] D --> R["性能特点"] R --> S["锁粒度细"] R --> T["并发度高"] R --> U["读操作无锁"]

Hashtable

  • 锁机制:使用synchronized关键字锁住整个表
  • 性能特点
    • 任何写操作都会阻塞整个表
    • 读操作也需要加锁
    • 并发度低,性能较差

Collections.synchronizedMap

  • 锁机制:使用synchronized关键字锁住整个Map对象
  • 性能特点
    • 任何写操作都会阻塞整个Map
    • 读操作也需要加锁
    • 并发度低,性能较差
    • 迭代时需要手动加锁

ConcurrentHashMap

  • 锁机制
    • JDK 1.7:分段锁(Segment)
    • JDK 1.8:CAS + synchronized
  • 性能特点
    • 锁粒度细,只锁住需要修改的部分
    • 读操作不需要加锁(通过volatile保证可见性)
    • 并发度高,性能较好
    • 支持复合操作的原子性(如putIfAbsent)

6. 总结

ConcurrentHashMap是Java并发包中一个重要的线程安全Map实现,它的设计目标是提供高并发性和高性能。

  • JDK 1.7及之前:采用分段锁(Segment)的设计,每个Segment是一个独立的哈希表,拥有自己的锁。这种设计允许多个线程同时访问不同的Segment,提高了并发性。

  • JDK 1.8及之后:放弃了分段锁的设计,转而采用CAS操作和synchronized关键字。锁的粒度更细,只锁住需要修改的桶,进一步提高了并发性。

ConcurrentHashMap的实现原理体现了Java并发编程的一些重要思想,如无锁算法、细粒度锁、并发扩容等。理解ConcurrentHashMap的实现原理,对于编写高性能的并发程序非常有帮助。

参考资料

  1. Java ConcurrentHashMap官方文档
  2. Java并发编程实战
  3. The Java® Virtual Machine Specification
  4. Java并发源码分析:ConcurrentHashMap
account_tree

思维导图

Interview AiBox logo

Interview AiBox — 面试搭档

不只是准备,更是实时陪练

Interview AiBox 在面试过程中提供实时屏幕提示、AI 模拟面试和智能复盘,让你每一次回答都更有信心。

AI 助读

一键发送到常用 AI

ConcurrentHashMap是Java并发包中的线程安全HashMap实现,旨在提供高并发性和高性能。JDK 1.7及之前采用分段锁(Segment)设计,每个Segment守护一部分数据,不同Segment的操作可并发执行。JDK 1.8及之后改为使用CAS操作和synchronized关键字,锁粒度更细,只锁住需要修改的桶,进一步提高了并发性。ConcurrentHashMap的读操作通常不需要加锁,写操作只锁定必要的部分,支持多线程并发扩容,整体性能远超Hashtable和Collections.synchronizedMap。其实现体现了无锁算法、细粒度锁、并发扩容等并发编程思想,是高性能并发编程的重要组件。

智能总结

深度解读

考点定位

思路启发

auto_awesome

相关题目

请解释线程池的概念、工作原理,以及它在实际应用中的优势。

线程池是一种多线程处理形式,通过预先创建和管理线程来提高系统性能。它的工作原理是:当任务到达时,从池中取出空闲线程执行任务,执行完毕后线程返回池中等待下次使用。线程池的核心优势包括:降低资源消耗(避免频繁创建销毁线程)、提高响应速度(无需等待线程创建)、提高线程可管理性(统一分配调优监控)以及提供更强大的功能(如定时执行)。合理配置线程池参数(核心线程数、最大线程数、存活时间、工作队列等)并遵循最佳实践(如使用有界队列、选择合适拒绝策略、优雅关闭等),可以充分发挥线程池的优势,广泛应用于Web服务器、数据库连接、异步任务处理和并行计算等场景。

arrow_forward

Java中有哪些类型的锁?请分别介绍它们的特点

Java中的锁主要可以从多个维度进行分类: 按特性分为悲观锁(假设数据会被修改,先加锁后操作)和乐观锁(假设数据不会被修改,更新时检查是否有修改,通常基于CAS实现)。 按实现方式分为synchronized关键字(内置锁,自动获取释放)和ReentrantLock(显式锁,功能更丰富,需手动释放)。 按锁状态分为偏向锁(无竞争时的优化)、轻量级锁(竞争不激烈时自旋尝试)和重量级锁(竞争激烈时线程阻塞)。 按功能分为可重入锁(同线程可多次获取)、读写锁(分离读写操作)、公平/非公平锁(按序分配或允许插队)、共享/排他锁(多线程共享或独占)。 Java并发包提供了多种锁实现:ReentrantLock(可重入独占锁)、ReentrantReadWriteLock(读写锁)、StampedLock(Java8新增,性能更高)、Condition(条件变量)和LockSupport(基本线程阻塞唤醒)。 选择锁时应考虑场景特点:简单同步用synchronized,需要高级功能用ReentrantLock,读多写少用读写锁,高并发低冲突用乐观锁,写频繁用悲观锁。

arrow_forward

请解释Java线程池的核心参数及其作用

Java线程池的核心参数包括7个关键配置:corePoolSize(核心线程数)控制常驻线程数量;maximumPoolSize(最大线程数)限制线程池最大容量;keepAliveTime和unit共同定义非核心线程的空闲存活时间;workQueue(工作队列)用于缓存待执行任务;threadFactory(线程工厂)统一创建线程;handler(拒绝策略)处理无法接收的任务。这些参数协同工作,决定了线程池的扩展性、资源利用率和任务处理能力。合理配置这些参数对系统性能至关重要,需根据任务类型(CPU密集型、IO密集型或混合型)和系统资源进行优化。

arrow_forward

在Java中,如何保证线程安全?

Java中保证线程安全有多种方法,包括使用synchronized关键字、Lock接口及其实现类、原子类、线程安全的集合类、volatile关键字、ThreadLocal、不可变对象设计以及正确使用线程池。每种方法都有其适用场景和优缺点。synchronized是最基本的同步机制,简单易用;Lock提供了更灵活的锁定操作;原子类利用CAS实现无锁算法;线程安全集合专为并发设计;volatile保证变量可见性;ThreadLocal实现线程间数据隔离;不可变对象天然线程安全;线程池有效管理并发任务。选择合适的方法应考虑具体场景,遵循最小化共享数据、优先使用局部变量、保持同步块简短等最佳实践。

arrow_forward

请解释Java中volatile关键字的作用和使用场景。

volatile是Java中用于保证变量可见性和有序性的轻量级同步机制。它确保一个线程对变量的修改对其他线程立即可见,并禁止指令重排序。与synchronized不同,volatile不保证原子性,不会阻塞线程,性能更高。volatile适用于状态标志位、一次性安全发布等场景,但不适用于需要原子性保证的复合操作。

arrow_forward

阅读状态

阅读时长

10 分钟

阅读进度

4%

章节:23 · 已读:0

当前章节: 1. 基本概念和设计目标

最近更新:2025-08-23

本页目录

Interview AiBox logo

Interview AiBox

AI 面试实时助手

面试中屏幕实时显示参考回答,帮你打磨表达。

免费下载download

分享题目

复制链接,或一键分享到常用平台

外部分享