ConcurrentHashMap 源码分析

ConcurrentHashMap 源码分析

ConcurrentHashMap 是 Java 并发包(java.util.concurrent)提供的线程安全、高效的哈希表实现,旨在替代线程安全但性能低下的 Hashtable(全局锁)和非线程安全的 HashMap。其核心设计目标是在保证线程安全的前提下,最大化并发访问效率。

本文将按 JDK 1.7 → JDK 1.8 的演进逻辑,从数据结构、核心方法、锁机制、性能优化等角度深入分析源码,揭示其设计思想。

一、核心特性总览

  1. 线程安全:通过分段锁(1.7)或 CAS + synchronized(1.8)实现,避免全局锁导致的并发瓶颈;
  2. 高效并发:支持多线程同时读写不同桶(或分段),仅对冲突节点加锁,锁粒度极小;
  3. 弱一致性迭代 :迭代器不抛出 ConcurrentModificationException,遍历的是当前数据的 "快照";
  4. 不支持 null 键 / 值:避免并发场景下无法区分 "key 不存在" 和 "value 为 null";
  5. 支持原子操作 :如 putIfAbsent()compute() 等,无需额外加锁。

二、JDK 1.7 实现:分段锁(Segment + HashEntry)

JDK 1.7 的 ConcurrentHashMap 核心是分段锁(Segment Lock) 机制,将哈希表拆分为多个独立的 "分段(Segment)",每个分段本质是一个独立的哈希表(ReentrantLock + HashEntry 数组 + 链表)。

1. 数据结构

java 复制代码
// ConcurrentHashMap 核心类(JDK 1.7)
public class ConcurrentHashMap<K, V> {
    // 分段数组(每个 Segment 是一个独立的锁和哈希表)
    final Segment<K, V>[] segments;
    // 每个分段的默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 分段数(默认 16,必须是 2 的幂,并发度默认 16)
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 分段类(继承 ReentrantLock,自带锁能力)
    static final class Segment<K, V> extends ReentrantLock implements Serializable {
        // 每个分段内部的 HashEntry 数组(链表数组)
        transient volatile HashEntry<K, V>[] table;
        // 分段内元素个数
        transient int count;
        // 扩容阈值(count >= threshold 时扩容)
        transient int threshold;
        // 负载因子
        final float loadFactor;
    }

    // 哈希节点(链表节点)
    static final class HashEntry<K, V> {
        final int hash;
        final K key;
        // value 用 volatile 保证可见性
        volatile V value;
        // next 用 volatile 保证链表修改的可见性
        volatile HashEntry<K, V> next;
    }
}
核心设计:
  • 分段锁隔离:每个 Segment 是一个独立的 ReentrantLock,线程操作某分段时,仅锁定该分段,其他分段可并发访问(并发度 = 分段数,默认 16);
  • volatile 保证可见性:HashEntry 的 value 和 next 用 volatile 修饰,避免线程间数据不可见;
  • 分段独立扩容:每个分段单独计算扩容阈值,扩容时仅锁定当前分段,不影响其他分段。

2. 核心方法分析

(1)put 方法:分段加锁插入
java 复制代码
public V put(K key, V value) {
    if (value == null) throw new NullPointerException(); // 不允许 null 值
    int hash = hash(key);
    // 计算 key 所属的分段索引(segments 数组的下标)
    int segmentIndex = (hash >>> segmentShift) & segmentMask;
    // 获取目标分段,若未初始化则初始化
    Segment<K, V> s = segmentFor(segmentIndex);
    // 分段内插入节点(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;
        // 计算 key 在分段内的桶索引
        int index = (tab.length - 1) & hash;
        HashEntry<K, V> first = entryAt(tab, index);
        for (HashEntry<K, V> e = first; ; ) {
            if (e != null) {
                //  key 已存在,更新 value
                K k;
                if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value; // volatile 写,保证可见性
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            } else {
                // key 不存在,插入新节点(链表头插法)
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<>(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;
}
关键逻辑:
  1. 计算 key 的哈希值,定位到所属分段;
  2. 尝试获取分段锁(tryLock()),失败则自旋重试(避免阻塞);
  3. 分段内定位桶索引,遍历链表:
    • 若 key 已存在,更新 value(volatile 写保证可见性);
    • 若 key 不存在,插入新节点(头插法);
  4. 检查分段内元素个数是否超过阈值,若超过则分段独立扩容;
  5. 释放锁。
(2)get 方法:无锁访问
java 复制代码
public V get(Object key) {
    int hash = hash(key);
    // 定位分段和桶索引
    return segmentFor(hash).get(key, hash);
}

// Segment 类的 get 方法
final V get(Object key, int hash) {
    if (count != 0) { // 分段内有元素才遍历
        HashEntry<K, V> e = getFirst(hash);
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value;
                if (v != null)
                    return v; // value 是 volatile,保证可见性
                // 若 value 为 null,可能是节点正在被删除,自旋重试
                return readValueUnderLock(e);
            }
            e = e.next; // next 是 volatile,保证链表遍历的可见性
        }
    }
    return null;
}
关键设计:
  • 无锁读取:get 方法无需加锁,依赖 HashEntry 的 value 和 next 是 volatile 修饰,保证线程间可见性;
  • 自旋重试:若 value 为 null(可能是节点正在被删除),则加锁后重新读取,避免读取中间状态。

3. JDK 1.7 缺点

  • 并发度有限:并发度 = 分段数(默认 16),若分段数固定,高并发下仍可能出现分段锁竞争;
  • 扩容效率低:分段独立扩容,但若某分段元素过多,扩容时会独占该分段锁,影响性能;
  • 链表查询效率低:无红黑树优化,哈希冲突严重时,链表查询时间复杂度为 O (n)。

三、JDK 1.8 实现:CAS + synchronized + 红黑树

JDK 1.8 对 ConcurrentHashMap 进行了彻底重构 ,放弃了分段锁,采用「数组 + 链表 + 红黑树」的结构(与 HashMap 一致),通过 CAS 无锁操作 + synchronized 细粒度锁 保证线程安全,同时引入红黑树优化哈希冲突。

1. 数据结构

java 复制代码
public class ConcurrentHashMap<K, V> {
    // 哈希表数组(volatile 修饰,保证数组扩容/初始化的可见性)
    transient volatile Node<K, V>[] table;
    // 扩容时的临时数组(transfer 期间使用)
    private transient volatile Node<K, V>[] nextTable;
    // 基础计数器(无竞争时使用)
    private transient volatile long baseCount;
    // 控制标志位(-1:正在初始化,-N:N-1 个线程正在扩容,正数:下次扩容阈值)
    private transient volatile int sizeCtl;

    // 核心节点(链表节点)
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        // value 和 next 用 volatile 保证可见性
        volatile V val;
        volatile Node<K, V> next;
    }

    // 红黑树节点(链表长度 >= 8 时转为红黑树)
    static final class TreeNode<K, V> extends Node<K, V> {
        TreeNode<K, V> parent; // 父节点
        TreeNode<K, V> left;   // 左子节点
        TreeNode<K, V> right;  // 右子节点
        TreeNode<K, V> prev;   // 链表前驱(用于红黑树转链表)
        boolean red;           // 红黑树颜色标记
    }

    // 扩容标记节点(ForwardingNode 所在的桶表示已迁移完成)
    static final class ForwardingNode<K, V> extends Node<K, V> {
        final Node<K, V>[] nextTable; // 指向新数组
        ForwardingNode(Node<K, V>[] tab) {
            super(MOVED, null, null, null); // hash = MOVED(-1)
            this.nextTable = tab;
        }
    }
}
核心设计:
  • 锁粒度细化 :放弃分段锁,直接对哈希桶的头节点加 synchronized 锁,仅阻塞同一桶的并发操作,不同桶完全并行;
  • CAS 无锁优化:初始化数组、插入首节点等场景用 CAS 操作,避免加锁;
  • 红黑树优化:链表长度 >= 8 时转为红黑树(查询时间复杂度从 O (n) 降至 O (log n));
  • 协助扩容:扩容时,其他线程访问到 ForwardingNode 会主动参与扩容,提升效率;
  • 原子计数:用 baseCount + CounterCell 数组实现原子计数,避免全局锁。

2. 核心方法分析

(1)initTable:初始化哈希表(CAS 无锁)
java 复制代码
private final Node<K, V>[] initTable() {
    Node<K, V>[] tab; int sc;
    // 循环 CAS 初始化,避免并发冲突
    while ((tab = table) == null || tab.length == 0) {
        // sizeCtl < 0 表示其他线程正在初始化,当前线程让出 CPU
        if ((sc = sizeCtl) < 0)
            Thread.yield(); 
        // CAS 将 sizeCtl 设为 -1(标记正在初始化)
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    // 初始化数组容量(默认 16)
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K, V>[] nt = (Node<K, V>[])new Node<?, ?>[n];
                    table = tab = nt;
                    // 计算扩容阈值(n * 0.75)
                    sc = n - (n >>> 2);
                }
            } finally {
                // 更新 sizeCtl 为扩容阈值
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
关键逻辑:
  • sizeCtl 控制初始化状态:sc > 0 表示未初始化(sc 为容量),sc = -1 表示正在初始化;
  • 并发初始化时,通过 CAS 竞争 sizeCtl 的修改权,失败的线程 yield 让出 CPU,避免忙等。
(2)putVal:核心插入方法(CAS + synchronized)
java 复制代码
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();
        // 桶位为空,CAS 插入首节点(无锁操作)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
                break; // CAS 成功则退出循环
        }
        // 桶位是 ForwardingNode(当前桶正在扩容),协助扩容
        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) { // 链表节点(hash >= 0)
                        binCount = 1;
                        for (Node<K, V> e = f;; ++binCount) {
                            K ek;
                            // key 已存在,更新 value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value; // volatile 写
                                break;
                            }
                            Node<K, V> pred = e;
                            // 遍历到链表尾部,插入新节点(尾插法)
                            if ((e = e.next) == null) {
                                pred.next = new Node<>(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;
                        }
                    }
                }
            }
            // 检查是否需要将链表转为红黑树(binCount >= 8)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i); // 链表转红黑树
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 统计元素个数(原子操作)
    addCount(1L, binCount);
    return null;
}
关键逻辑拆解:
  1. 二次哈希spread(hashCode) 将 hashCode 高位与低位混合,减少哈希冲突;
  2. 初始化检查 :若哈希表未初始化,调用 initTable() 初始化;
  3. 无锁插入首节点 :桶位为空时,用 casTabAt()(CAS 操作)插入节点,避免加锁;
  4. 协助扩容 :若桶位是 ForwardingNode(hash = MOVED),调用 helpTransfer() 参与扩容;
  5. 细粒度锁:对桶的头节点加 synchronized 锁,保证同一桶的并发安全;
  6. 链表 / 红黑树插入:
    • 链表节点(hash >= 0):遍历链表,存在则更新,不存在则尾插;
    • 红黑树节点(TreeBin):调用红黑树插入逻辑;
  7. 链表转红黑树 :链表长度 >= 8 时,调用 treeifyBin() 转为红黑树;
  8. 原子计数 :调用 addCount() 更新元素个数。
(3)get 方法:无锁读取(volatile 保证可见性)
java 复制代码
public V get(Object key) {
    Node<K, V>[] tab; Node<K, V> e, p; int n, eh; K ek;
    int hash = spread(key.hashCode());
    // 哈希表不为空,且桶位存在节点
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & hash)) != null) {
        // 头节点就是目标节点,直接返回
        if ((eh = e.hash) == hash) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 红黑树节点(eh < 0 且 e 是 TreeBin)
        else if (eh < 0)
            return (p = e.find(hash, key)) != null ? p.val : null;
        // 链表节点,遍历查找
        while ((e = e.next) != null) {
            if (e.hash == hash &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
关键设计:
  • 无锁读取:依赖 Node 的 val 和 next 是 volatile 修饰,保证线程间可见性;
  • 快速定位:
    • 头节点匹配直接返回;
    • 红黑树节点调用 find() 方法查找(O (log n));
    • 链表节点遍历查找(O (n),但红黑树优化后概率极低)。
(4)transfer:扩容方法(多线程协助扩容)

扩容是 ConcurrentHashMap 1.8 的核心优化之一,支持多线程同时扩容,流程如下:

  1. 触发条件:元素个数 >= sizeCtl(扩容阈值);
  2. 初始化新数组:新数组容量 = 原数组容量 * 2;
  3. 标记扩容状态 :将 sizeCtl 设为 -N(N 为参与扩容的线程数);
  4. 迁移桶数据:对每个桶加锁,将原数组的桶数据迁移到新数组的两个桶(因为容量翻倍,哈希值高位参与计算);
  5. 标记迁移完成:迁移后的桶插入 ForwardingNode,告知其他线程该桶已迁移;
  6. 协助扩容 :其他线程访问到 ForwardingNode 时,会调用 helpTransfer() 参与迁移,直到所有桶迁移完成。

3. 原子计数:baseCount + CounterCell

JDK 1.8 用「基础计数器 + 单元格数组」实现原子计数,避免全局锁:

  • 无竞争场景 :直接用 CAS 更新 baseCount
  • 高竞争场景 :若 CAS 失败,创建 CounterCell 数组,每个线程更新不同的 CounterCell,最后求和得到总元素个数。
java 复制代码
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 尝试 CAS 更新 baseCount,失败则操作 CounterCell
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // CounterCell 数组未初始化,或当前线程的 Cell  CAS 失败
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 初始化 CounterCell 或处理竞争
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount(); // 求和 baseCount + 所有 CounterCell 的值
    }
    // 检查是否需要扩容
    if (check >= 0) {
        Node<K, V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                // CAS 增加参与扩容的线程数(sc += 1)
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 标记扩容开始(sizeCtl = rs << RESIZE_STAMP_SHIFT + 2)
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

四、JDK 1.7 vs JDK 1.8 核心差异

对比维度 JDK 1.7 JDK 1.8
数据结构 Segment 数组 + HashEntry 链表 Node 数组 + 链表 / 红黑树
锁机制 分段锁(ReentrantLock) CAS + synchronized(桶头节点锁)
并发度 固定(默认 16,分段数) 动态(与桶数一致,默认 16)
扩容机制 分段独立扩容 多线程协助扩容
哈希冲突优化 仅链表(O (n)) 链表转红黑树(O (log n))
计数方式 分段 count 求和(加锁) baseCount + CounterCell(原子)
锁粒度 粗粒度(分段) 细粒度(桶)

五、常见问题与总结

1. 为什么 ConcurrentHashMap 不支持 null 键 / 值?

  • 并发场景下,get(key) 返回 null 无法区分 "key 不存在" 和 "value 为 null";
  • HashMap 支持 null 是因为非线程安全,可通过 containsKey(key) 辅助判断,但 ConcurrentHashMap 并发环境下 containsKeyget 之间可能存在数据修改,无法保证一致性。

2. 与 Hashtable 的区别?

  • 锁机制:Hashtable 是全局锁(synchronized 修饰方法),ConcurrentHashMap 是细粒度锁(1.8);
  • 性能:ConcurrentHashMap 支持高并发,Hashtable 并发性能极差;
  • null 支持:Hashtable 支持 null 键 / 值,ConcurrentHashMap 不支持;
  • 迭代器:Hashtable 迭代器是快速失败(抛 ConcurrentModificationException),ConcurrentHashMap 是弱一致性。

3. 核心总结

  • 演进逻辑:从 "分段锁隔离" 到 "CAS + 细粒度 synchronized",锁粒度更细,并发度更高;
  • 性能优化:红黑树优化哈希冲突、多线程协助扩容、无锁读取、原子计数,最大化并发效率;
  • 设计思想:在保证线程安全的前提下,通过 "锁粒度最小化 + 无锁操作 + 协助机制" 提升并发性能,是 Java 并发编程中哈希表的首选实现。
相关推荐
CHANG_THE_WORLD8 小时前
Python 中的循环结构详解
开发语言·python·c#
JS_GGbond8 小时前
JavaScript入门学习路线图
开发语言·javascript·学习
quikai19819 小时前
python练习第一组
开发语言·python
BD_Marathon9 小时前
【JavaWeb】JS_JSON在客户端的使用
开发语言·javascript·json
ChrisitineTX9 小时前
凌晨突发Java并发问题:synchronized锁升级导致接口超时,排查过程全记录
java·数据库·oracle
还没想好取啥名9 小时前
C++11新特性(一)——原始字面量
开发语言·c++
谷粒.9 小时前
测试数据管理难题的7种破解方案
运维·开发语言·网络·人工智能·python
zzhongcy9 小时前
Java: HashMap 和 ConcurrentHashMap的区别
java·开发语言
✎ ﹏梦醒͜ღ҉繁华落℘9 小时前
菜鸟的算法基础
java·数据结构·算法