# ConcurrentHashMap 深度分析

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


基于JDK11进行分析, 大部分公司已经开始使用JDK11

ConcurrentHashMap 是 Java 并发包中核心的线程安全哈希表实现,设计用于高并发环境下的读写访问,避免全表锁的性能瓶颈。 它采用了分段锁思想的演进版本,底层结构和实现细节较 HashMap 有较大区别。

# 一、继承体系

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable
1
2
类/接口 作用 说明
AbstractMap 抽象骨架 提供基本的 Map 实现骨架
ConcurrentMap 并发行为规范 定义线程安全的 Map 额外方法
Serializable 接口能力 支持序列化

# 二、底层数据结构解析

  • ConcurrentHashMap 采用了 数组 + 链表 + 红黑树 结构(JDK8 及以后版本)
  • 核心是对数组中每个桶节点进行无锁读取分段锁写入的设计,提升并发性能
  • JDK11 已经抛弃了 JDK7 的分段锁(Segment)设计,改用 CAS + synchronized 结合 Node链表/树 的方式实现细粒度同步

# 核心属性

transient volatile Node<K,V>[] table; // 主数组,volatile 保证可见性
transient volatile Node<K,V>[] nextTable; // 扩容时新数组引用
transient volatile long baseCount; // 元素计数,部分线程竞争时辅助计数
transient volatile int sizeCtl;   // 控制扩容的阈值或初始化标志
transient volatile int transferIndex; // 扩容时分配给线程的迁移桶索引
1
2
3
4
5

# Node 节点定义

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;      // key 的哈希值
    final K key;
    volatile V val;      // value 使用 volatile 保证可见性
    volatile Node<K,V> next;  // 下一个节点
}
1
2
3
4
5
6
  • HashMapNode 类似,但 valnext 都是 volatile,保证读写线程安全

  • Node 本身不含锁,采用 CAS 操作实现无锁插入和链表修改

红黑树相关知识在HashMap章节已经充分介绍, 此处不再介绍


# 三、主要设计亮点

# CAS + synchronized 混合锁策略

  • 无锁读(直接读取 volatile 变量)
  • 写操作中,先通过 CAS 尝试插入(无锁快路径),失败时才用 synchronized 加锁对应链表头节点或树节点,避免全表锁
  • 扩容时,多个线程协同迁移桶,通过 transferIndex 变量分配任务,支持多线程并发扩容

# 延迟初始化

  • 数组延迟初始化,直到第一次插入才创建底层数组,减少资源浪费

# 树化与链表策略

  • HashMap,当链表长度超过阈值时(默认 8),转成红黑树
  • 树化条件更复杂,保证并发安全的同时提升查询效率

# 扩容机制

  • 扩容为原容量的两倍
  • 使用多线程协作迁移元素(与 HashMap 仅单线程扩容不同)
  • 迁移过程中,使用特殊节点 ForwardingNode 作为标记,指示该桶已迁移,帮助新线程正确访问新表

# 四、核心方法解析(JDK11)

#

put(K key, V value)

    public V put(K key, V value) {
        return putVal(key, value, false);
    }


    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; K fk; V fv;
            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)))
                    break;                  
            }
            // 扩容检测与协助
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else if (onlyIfAbsent // check first node without acquiring lock
                     && fh == hash
                     && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                     && (fv = f.val) != null)
                return fv;
            // 加锁处理链表或树结构
            else {
                V oldVal = null;
                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);
                                    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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

扰动计算(spread) 首先,通过 spread(key.hashCode()) 计算 key 的哈希值,这个方法会对原始 hash 码做一次扰动处理(异或高16位),使得哈希分布更加均匀,减少哈希冲突。

初始化数组 接下来进入循环中,尝试获取当前的哈希表数组 table。如果 table 为空或长度为 0,则调用 initTable() 来初始化容量(默认16),保证数组存在,准备存放节点。

定位桶槽 使用 (n - 1) & hash 快速定位 key 应该存放的桶槽下标 i,利用数组长度减一与哈希值做位与运算,快速定位槽位。

空桶CAS插入 判断 tab[i] 位置是否为空。如果为空,利用 CAS(Compare And Swap)原子操作尝试将新节点直接插入桶位。这个步骤是关键优化,避免无谓的锁操作,提高并发性能。

扩容检测与协助 如果发现当前桶的头节点 fhash 值等于 MOVED(特殊标志,代表正在扩容),说明哈希表正在进行扩容迁移。此时不会插入,而是调用 helpTransfer 方法协助扩容完成,避免阻塞写操作。

加锁处理链表或树结构 若桶槽非空且非扩容中,进入同步块,锁住桶头节点,避免并发修改。

  • 链表情况:遍历链表,查找是否已有相同 key 的节点。如果找到了,且不是只在不存在时插入(onlyIfAbsent == false),则更新该节点的 value 并返回旧值。
  • 如果链表中没有相同 key 节点,则在链表尾部插入新节点。
  • 同时计数桶中节点数,用于后续判断是否转换为红黑树。

红黑树插入 如果桶头是 TreeBin(树形结构)节点,则调用红黑树的 putTreeVal 方法插入新节点或更新已有节点,保证插入后树结构的平衡和有序。

更新计数器与扩容检查 插入或更新节点后,调用 addCount(1, binCount) 方法,原子地增加元素计数器,同时根据链表或树节点数判断是否需要触发扩容操作(默认链表超过8个节点会转树,大小超过阈值会扩容)。

返回结果 如果是更新已有 key 的值,返回旧值;如果是新插入,则返回 null。

#

public V remove(Object key) {
    return replaceNode(key, null, null);
}


final V replaceNode(Object key, V value, Object cv) {
    // 扰动函数
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 若 tab 未初始化或该桶为空,直接 break 返回 null(没有要删除的元素)。
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        
        // 如果桶头 f.hash == MOVED,说明该桶正在被迁移(扩容过程中),
        //本线程会调用 helpTransfer 协助迁移,然后重新取 tab 再试。这样能避免阻塞并尽快完成迁移。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            // 桶头 f 为锁, 加上同步块
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 处理链表
                    if (fh >= 0) {
                        validated = true;
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    // 处理树
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }

            if (validated) {
                // 找到了目标并已删除/替换
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                // 若没有找到(oldVal == null),break 出循环并最终返回 null(表示未命中)。
                break;
            }
        }
    }
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
  • 无锁快速路径tabAt、CAS 在空桶直接插入/替换时使用(这是 put 的优化;remove 则在空桶直接返回)。

  • 桶粒度锁:对链表/树的多步修改使用 synchronized(f),锁粒度很小(单个桶),降低竞争范围。

  • 二次校验:加锁后用 if (tabAt(tab, i) == f) 校验,避免在加锁前后桶头被其他线程替换导致错误(防 ABA)。

  • MOVED 与 helpTransfer:遇到扩容迁移,当前线程会协助 helpTransfer,保证迁移并发安全与进度。

  • setTabAt / volatile 保证可见性:对桶头的写入通过 setTabAt(volatile 写)确保其他线程能立即看到结果;链表内部的 next 指针通常通过锁操作保证可见性。

  • 返回语义:若找到并删除/替换,返回找到的旧值;否则返回 null。删除成功会减少计数。

#

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 没啥好说的, 比较简单, 但是需要注意在并发场景下查询可能同一个key先后查询的值不一样, 因为变更是加锁操作不可见, 这是正常的现象, 本身ConcurrentHashMap是为了保证高并发+线程安全, 没有保证强一致性, 如果要保证强一致性会导致性能下降。

# 再分配

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

再分配代码也比较简单, 但是触发时机和HashMap不同

// HashMap
put(...) {
  // 插入节点
  if (++size > threshold) {
    resize(); // 一次性搬迁整个表
  }
}

// ConcurrentHashMap
putVal(...) {
  if (table == null) initTable();
  if (tab[i] is ForwardingNode) {
    helpTransfer(tab, f); // 看到正在迁移,来帮忙
  }
  // 插入(空位用 CAS,非空用 synchronized(f))
  if (needExpand) { // 线程A触发扩容
    // 将 sizeCtl 设为负,建立 nextTable 并开始 transfer()
    // 但 transfer() 会划分区间,其他线程也能来帮助
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

HashMap

  • putVal/put 内有 if (++size > threshold) resize();resize() 会创建新数组并对所有旧节点做重哈希搬迁(一次性完成)。

ConcurrentHashMap

  • putVal 里有 if (tab == null) initTable()、并且当遇到 f.hash == MOVEDhelpTransfer(tab, f)
  • helpTransfer()sizeCtlresizeStamptransferIndexForwardingNode.nextTable 等字段协调多线程搬迁(见 helpTransfer 实现)。
  • transfer() 本身包含对 transferIndex 的 CAS 划分任务、只搬一个区段的逻辑。

# 五、常见问题

# 为什么 put 方法中部分用 CAS,部分用 synchronized

原因

  • 空桶插入:如果对应桶位置是 null,则直接使用 CAS(compareAndSet) 将新节点放入,这样避免加锁,提高并发性能。
  • 桶中已有节点:需要遍历链表或红黑树来插入新元素,此时必须保证遍历+修改是原子性的,所以对桶的头节点加 synchronized 锁(锁粒度仅限于单个桶)。

优点

  • CAS 无锁化空桶插入,减少锁竞争。
  • 分段锁(桶锁)而非全表锁,提高并发度。

# 查询 (get) 为什么不加锁?

原因

  • ConcurrentHashMap 内部节点(Node)的 valnext 都是 volatile 修饰,保证可见性。
  • 读操作只会读取节点的引用和 val,不会修改结构,因此不需要加锁。
  • 如果有写线程在并发修改,读线程可能会读到旧值或新值,但不会读到中间状态(结构一致性由 volatile 和写锁保证)。

影响

  • 多线程并发读同一个 key,有可能 A 读到旧值,B 读到新值,这是允许的,因为它是弱一致性设计(以换取性能)。
  • 对于需要强一致性的场景,需要额外的同步机制。

# 为什么在 put 方法中没看到明显的 resize 调用?

原因

  • putVal 方法中确实会触发扩容,但不是像 HashMap 一样直接 resize() 一次性完成。
  • 当检测到需要扩容时(binCount >= TREEIFY_THRESHOLDsize > sizeCtl):
    1. 会初始化 nextTable
    2. sizeCtl 置为负值,标记“正在扩容”。
    3. 把桶位置设置成 ForwardingNode(特殊节点),表示该桶已经迁移或正在迁移。
    4. 执行 transfer(),搬运部分桶的数据。

# ConcurrentHashMap 的分布式扩容是怎么做的?

核心机制

  • 多线程协作扩容:不是由一个线程搬完全部数据,而是多个线程分段搬迁
  • 关键字段
    • transferIndex:全局递减计数器,标记剩余未搬迁的桶索引。
    • sizeCtl:负值表示正在扩容,低 16 位是并发搬迁的线程数,高 16 位是 resizeStamp(扩容唯一标识)。
    • ForwardingNode:桶迁移占位符,表示数据已搬走或正在搬走。

协作过程(helpTransfer 方法)

  1. 线程 A 触发扩容 → 创建 nextTable → 设置标志位。
  2. 线程 B 访问到 ForwardingNode,调用 helpTransfer 参与扩容。
  3. 每个线程通过 CAS 争夺一段区间(transferIndex)并搬迁。
  4. 所有桶搬迁完成后,将 nextTable 设为 table 并清理标志。

优点

  • 搬迁工作分摊给多个线程,减少单线程阻塞时间。
  • 在迁移期间,读写仍然可以进行(写会优先迁移目标桶的数据)。

# 为什么 ConcurrentHashMap 允许 Key/Value 为 null 的行为和 HashMap 不一样?

  • Key 不允许为 null
    • 无法区分 map.get(key) == null 是 key 不存在,还是 value 为 null。
  • Value 不允许为 null
    • 也是为了简化并发条件下的逻辑判断,避免 null 引起的二义性。

# ConcurrentHashMapHashMap 扩容的最大不同?

特性 HashMap ConcurrentHashMap
扩容触发 put 时判断 size > threshold put 时判断 binCount 或 sizeCtl
扩容方式 单线程一次性完成 多线程分段搬迁(渐进式)
扩容期间读写 全表阻塞(如果外部加锁) 允许读,写会协助迁移
性能 低并发场景简单高效 高并发场景扩容无长暂停

# 为什么扩容期间不会出现死循环或数据丢失?

  • 使用 ForwardingNode 占位,防止新旧表同时被访问时造成链表环或重复迁移。
  • 每个桶只会被一个线程迁移一次,迁移完成后立即替换成 ForwardingNode
  • volatile + CAS 保证迁移的可见性和原子性。