🔄 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 小时前
Spring Boot API文档与自动化测试详解
java·spring boot·后端
数据小馒头4 小时前
Web原生架构 vs 传统C/S架构:在数据库管理中的性能与安全差异
后端
用户68545375977694 小时前
🔑 AQS抽象队列同步器:Java并发编程的"万能钥匙"
后端
yren4 小时前
Mysql 多版本并发控制 MVCC
后端
回家路上绕了弯4 小时前
外卖员重复抢单?从技术到运营的全链路解决方案
分布式·后端
考虑考虑4 小时前
解决idea导入项目出现不了maven
java·后端·maven
数据飞轮4 小时前
不用联网、不花一分钱,这款开源“心灵守护者”10分钟帮你建起个人情绪疗愈站
后端
Amos_Web4 小时前
Rust实战课程--网络资源监控器(初版)
前端·后端·rust
程序猿小蒜4 小时前
基于springboot的基于智能推荐的卫生健康系统开发与设计
java·javascript·spring boot·后端·spring