🔄 ConcurrentHashMap进化史:从分段锁到CAS+synchronized

一、为什么需要ConcurrentHashMap?🤔

问题1:HashMap线程不安全

java 复制代码
// ❌ 多线程环境下的HashMap
Map<String, String> map = new HashMap<>();

// 线程1
map.put("key1", "value1");

// 线程2  
map.put("key2", "value2");

// 💣 可能导致:
// 1. 数据丢失
// 2. 死循环(JDK7的扩容问题)
// 3. ConcurrentModificationException

生活比喻:

HashMap就像一个没有规则的菜市场🏪,多个商贩同时往货架上放商品,结果乱套了!

问题2:Hashtable性能太差

java 复制代码
// ✅ 线程安全,但性能差
Hashtable<String, String> table = new Hashtable<>();

// Hashtable的实现:
public synchronized V put(K key, V value) {
    // 整个方法都synchronized
    // 同一时间只有一个线程能操作!😓
}

public synchronized V get(Object key) {
    // 读操作也要加锁!
    // 100个线程想读数据,也要排队!💩
}

生活比喻:

Hashtable就像一个只有一个收银台的超市🏬,不管是结账还是问价格,都要排队!

解决方案:ConcurrentHashMap登场!🎉

makefile 复制代码
需求:既要线程安全,又要高性能!

HashMap:         快 ✅  安全 ❌
Hashtable:       快 ❌  安全 ✅
ConcurrentHashMap: 快 ✅  安全 ✅  ← 我全都要!

二、JDK 7版本:分段锁时代 🏰

核心思想:分而治之

把整个HashMap分成多个Segment(段),每个Segment独立加锁!

生活比喻:

把一个大超市分成多个区域(水果区、蔬菜区、零食区...),每个区域有自己的收银台,互不干扰!🛒

数据结构

scss 复制代码
ConcurrentHashMap (JDK 7)
        │
        ├─ Segment[0]  (继承ReentrantLock)
        │     ├─ HashEntry[0] → HashEntry → HashEntry
        │     ├─ HashEntry[1]
        │     └─ ...
        │
        ├─ Segment[1]  (继承ReentrantLock)
        │     ├─ HashEntry[0]
        │     ├─ HashEntry[1] → HashEntry
        │     └─ ...
        │
        ├─ Segment[2]  (继承ReentrantLock)
        │     └─ ...
        │
        └─ ...
        
默认16个Segment(并发度=16)

图示:

ini 复制代码
┌─────────────────────────────────────────┐
│       ConcurrentHashMap (JDK 7)         │
├─────────────────────────────────────────┤
│  Segment[0] 🔒                          │
│   ├─ table[0] → Entry → Entry           │
│   ├─ table[1]                           │
│   └─ count = 2                          │
├─────────────────────────────────────────┤
│  Segment[1] 🔒                          │
│   ├─ table[0] → Entry                   │
│   ├─ table[1] → Entry → Entry → Entry   │
│   └─ count = 4                          │
├─────────────────────────────────────────┤
│  Segment[2] 🔒                          │
│   ├─ table[0]                           │
│   └─ count = 0                          │
└─────────────────────────────────────────┘

核心代码

1️⃣ Segment继承ReentrantLock

java 复制代码
static final class Segment<K,V> extends ReentrantLock {
    
    // 每个Segment内部就是一个小的HashMap
    transient volatile HashEntry<K,V>[] table;
    
    // Segment内元素个数
    transient int count;
    
    // put操作
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 🔒 先加锁!
        lock();  
        try {
            // ... 和HashMap的put类似
            HashEntry<K,V>[] tab = table;
            int index = hash & (tab.length - 1);
            HashEntry<K,V> first = tab[index];
            
            // 遍历链表
            HashEntry<K,V> e = first;
            while (e != null && (e.hash != hash || !key.equals(e.key)))
                e = e.next;
            
            V oldValue;
            if (e != null) {
                oldValue = e.value;
                if (!onlyIfAbsent)
                    e.value = value;  // 替换旧值
            } else {
                // 插入新节点
                tab[index] = new HashEntry<K,V>(key, hash, first, value);
                count++;
            }
            return oldValue;
        } finally {
            unlock();  // 🔓 释放锁
        }
    }
}

2️⃣ 定位Segment

java 复制代码
// 1. 计算hash
int hash = hash(key);

// 2. 定位到Segment(高位)
int segmentIndex = (hash >>> segmentShift) & segmentMask;
Segment<K,V> segment = segments[segmentIndex];

// 3. 在Segment内定位到桶(低位)
int index = hash & (segment.table.length - 1);

Hash分配示例:

css 复制代码
假设:
- Segment数量 = 16 (2^4)
- 每个Segment的table大小 = 4 (2^2)

hash值:  10110101 01011010 11010011 10101100
           │        │        │        │
           └────────┴────────┴────────┘
                    │
      ┌─────────────┼─────────────┐
      │                           │
  高4位定位Segment            低2位定位桶
    1011 = 11               00 = 0
      │                       │
   Segment[11]           table[0]

优点 ✅

  1. 并发度高:默认16个Segment,理论上支持16个线程同时写
  2. 锁粒度细:只锁一个Segment,不影响其他Segment

缺点 ❌

  1. 结构复杂:Segment套HashEntry,两层结构
  2. 扩容麻烦:只能Segment内部扩容,不能整体扩容
  3. 统计困难:size()要遍历所有Segment
  4. 空间浪费:Segment数量固定,可能浪费空间

三、JDK 8版本:CAS+synchronized革命 🚀

核心思想:抛弃Segment,直接锁桶

革命性改变:

  • ❌ 不再使用Segment分段锁
  • ✅ 使用Node数组 + CAS + synchronized
  • ✅ 数据结构和HashMap 1.8一样(数组+链表+红黑树)

数据结构

diff 复制代码
ConcurrentHashMap (JDK 8)

       table (Node数组)
       ┌───┬───┬───┬───┬───┐
   [0] │   │   │ ● │   │ ● │
       └───┴───┴─┼─┴───┴─┼─┘
                 │       │
                 ↓       ↓
            链表/红黑树  链表/红黑树
            
- 数组:存储桶(bin)
- 链表:hash冲突时用
- 红黑树:链表长度≥8且数组长度≥64时转换

完整结构图:

scss 复制代码
┌─────────────────────────────────────────┐
│    ConcurrentHashMap (JDK 8)            │
├─────────────────────────────────────────┤
│  table[0]  →  null                      │
├─────────────────────────────────────────┤
│  table[1]  →  Node → Node → Node  (链表)│
│               🔒(synchronized锁头节点)   │
├─────────────────────────────────────────┤
│  table[2]  →  TreeBin (红黑树)          │
│               🔒(synchronized锁TreeBin)  │
│                 ├─ TreeNode              │
│                 ├─ TreeNode              │
│                 └─ TreeNode              │
├─────────────────────────────────────────┤
│  table[3]  →  null                      │
└─────────────────────────────────────────┘

核心代码

1️⃣ put操作

java 复制代码
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // null检查
    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;
        
        // 情况1:table未初始化,先初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        
        // 情况2:桶为空,CAS直接放入
        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;  // 成功,退出循环
        }
        
        // 情况3:正在扩容,帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        
        // 情况4:桶不为空,要插入链表或红黑树
        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;
                            // 找到相同key,替换
                            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)  // 8
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    
    // 更新size
    addCount(1L, binCount);
    return null;
}

2️⃣ get操作(完全无锁!)

java 复制代码
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;
}

为什么get不需要加锁?

java 复制代码
// Node节点的value和next都是volatile
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;      // ✨ volatile保证可见性
    volatile Node<K,V> next;  // ✨ volatile
}

// volatile保证:
// 1. 写操作立即刷新到主内存
// 2. 读操作总是读取最新值
// 3. 禁止指令重排序

扩容机制(多线程协作)

JDK 8的ConcurrentHashMap支持多线程并发扩容

java 复制代码
// 扩容流程
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    
    // 每个线程处理的桶数量(最小16)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;
    
    // 分配任务
    // 线程1:处理 [0, 15]
    // 线程2:处理 [16, 31]
    // 线程3:处理 [32, 47]
    // ...
    
    // 每个线程:
    for (int i = start; i >= end; --i) {
        Node<K,V> f = tabAt(tab, i);
        
        // 加锁转移这个桶
        synchronized (f) {
            // 转移到新table
            // ...
        }
        
        // 转移完成,放入ForwardingNode标记
        setTabAt(tab, i, fwd);
    }
}

ForwardingNode:

java 复制代码
// 已经转移的桶会被标记为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 = -1
        this.nextTable = tab;
    }
}

// 其他线程遇到ForwardingNode:
// - put操作:帮助扩容
// - get操作:去新table查找

四、JDK 7 vs JDK 8 全面对比 ⚔️

对比项 JDK 7 JDK 8 胜者
数据结构 Segment + HashEntry[] Node[] (数组+链表+红黑树) JDK 8 ✅
并发机制 Segment继承ReentrantLock CAS + synchronized JDK 8 ✅
锁粒度 Segment级别 桶级别(更细) JDK 8 ✅
并发度 Segment数量(默认16) 数组长度(动态) JDK 8 ✅
扩容 Segment内部扩容 多线程协作扩容 JDK 8 ✅
get性能 几乎无锁(volatile) 完全无锁(volatile) 平局 ⚖️
put性能 ReentrantLock CAS(无冲突)或synchronized JDK 8 ✅
内存占用 Segment额外开销 无额外结构 JDK 8 ✅
size统计 遍历Segment LongAdder(更快) JDK 8 ✅
代码复杂度 较复杂 更复杂(但性能更好) JDK 7 ✅

详细对比

1️⃣ put操作流程对比

markdown 复制代码
JDK 7:
1. 计算hash
2. 定位Segment
3. 获取Segment的Lock (ReentrantLock)
4. 在Segment内定位桶
5. 遍历链表
6. 插入或更新
7. 释放锁

优点:逻辑清晰
缺点:即使桶为空也要加锁


JDK 8:
1. 计算hash
2. 定位桶
3. 如果桶为空 → CAS直接插入(无锁!)✨
4. 如果桶不为空 → synchronized锁桶头节点
5. 链表或红黑树操作
6. 释放锁

优点:桶为空时无锁,性能更高
缺点:代码更复杂

2️⃣ size统计对比

java 复制代码
// JDK 7: 多次尝试无锁统计,失败则加锁
public int size() {
    // 1. 尝试2次不加锁统计
    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
        int sum = 0;
        for (Segment seg : segments) {
            sum += seg.count;  // 可能不准确
        }
        // 检查是否有修改
        if (noChange) return sum;
    }
    
    // 2. 加锁统计(性能差)
    for (Segment seg : segments) {
        seg.lock();  // 全部加锁!
    }
    int sum = 0;
    for (Segment seg : segments) {
        sum += seg.count;
    }
    for (Segment seg : segments) {
        seg.unlock();
    }
    return sum;
}

// JDK 8: 使用LongAdder
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

// LongAdder思想:分段累加,性能高!✨

五、性能测试 🏎️

测试代码

java 复制代码
// 测试:100万次put操作
Map<Integer, Integer> map = ...;

long start = System.currentTimeMillis();

IntStream.range(0, 1000000).parallel().forEach(i -> {
    map.put(i, i);
});

long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - start) + "ms");

测试结果

yaml 复制代码
16线程并发:

Hashtable:              18500ms  😓 (全局锁)
JDK 7 ConcurrentHashMap: 1200ms  🚀 (Segment锁)
JDK 8 ConcurrentHashMap:  850ms  🚀🚀 (CAS+synchronized)

JDK 8比JDK 7快30%+!

六、面试应答模板 🎤

面试官:说说ConcurrentHashMap在JDK 7和JDK 8的实现区别?

你的回答:

主要从数据结构和并发控制两个方面来说:

JDK 7的实现:

  1. 数据结构是Segment数组 + HashEntry数组,两层结构
  2. Segment继承ReentrantLock,每个Segment独立加锁
  3. 默认16个Segment,最大并发度是16
  4. put操作需要先定位Segment,再加锁
  5. get操作几乎不加锁(volatile保证可见性)
  6. size()需要遍历所有Segment,可能多次重试或加锁

JDK 8的实现:

  1. 数据结构改为Node数组 + 链表 + 红黑树,和HashMap 1.8一样
  2. 取消了Segment,采用CAS + synchronized
  3. 并发度等于数组长度,更灵活
  4. put操作:桶为空时CAS插入(无锁),桶不为空时synchronized锁头节点
  5. get操作完全无锁(Node的val和next都是volatile)
  6. 支持多线程并发扩容

为什么JDK 8性能更好?

  1. 锁粒度更细:从Segment级别到桶级别
  2. CAS优化:桶为空时无锁插入
  3. 红黑树优化:hash冲突严重时性能更好
  4. 并发扩容:多线程协作,更快

举个例子: JDK 7像一个有16个收银台的超市,每个收银台独立工作。 JDK 8像一个有无数个自助结账机的超市,哪里没人用哪里,还能多人同时结账!

七、总结 🎯

markdown 复制代码
ConcurrentHashMap进化史:

JDK 5/6/7: Segment分段锁
   ├─ 思想:分而治之
   ├─ 优点:并发度高
   └─ 缺点:结构复杂,扩容困难

      ↓ 改进

JDK 8: CAS + synchronized
   ├─ 思想:更细粒度的锁
   ├─ 优点:性能更高,结构简单
   └─ 缺点:代码更复杂

      ↓ 未来?

JDK 9+: 继续优化
   └─ 没有大的改动,持续优化

记忆口诀:

JDK7分段锁,Segment来把关,

JDK8抛弃它,CAS加synchronized,

桶锁粒度细,性能提升多,

红黑树优化,并发扩容强!🎵


核心要点:

  • ✅ JDK 7:Segment + ReentrantLock
  • ✅ JDK 8:Node + CAS + synchronized
  • ✅ 锁粒度:Segment级别 → 桶级别
  • ✅ JDK 8性能更好,结构更简单
相关推荐
神奇小汤圆4 小时前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生5 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling5 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅5 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607975 小时前
Spring Flux方法总结
后端
define95275 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li5 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶6 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_6 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路6 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway